# UI Testing Guide ## Overview This directory contains Selenium-based UI tests for NetAlertX. Tests validate both API endpoints and browser functionality. ## Test Types ### 1. Page Load Tests (Basic) ```python def test_page_loads(driver): """Test: Page loads without errors""" driver.get(f"{BASE_URL}/page.php") time.sleep(2) assert "fatal" not in driver.page_source.lower() ``` ### 2. Element Presence Tests ```python def test_button_present(driver): """Test: Button exists on page""" driver.get(f"{BASE_URL}/page.php") time.sleep(2) button = driver.find_element(By.ID, "myButton") assert button.is_displayed(), "Button should be visible" ``` ### 3. Functional Tests (Button Clicks) ```python def test_button_click_works(driver): """Test: Button click executes action""" driver.get(f"{BASE_URL}/page.php") time.sleep(2) # Find button button = driver.find_element(By.ID, "myButton") # Verify it's clickable assert button.is_enabled(), "Button should be enabled" # Click it button.click() # Wait for result time.sleep(1) # Verify action happened (check for success message, modal, etc.) success_msg = driver.find_elements(By.CSS_SELECTOR, ".alert-success") assert len(success_msg) > 0, "Success message should appear" ``` ### 4. Form Input Tests ```python def test_form_submission(driver): """Test: Form accepts input and submits""" driver.get(f"{BASE_URL}/form.php") time.sleep(2) # Fill form fields name_field = driver.find_element(By.ID, "deviceName") name_field.clear() name_field.send_keys("Test Device") # Select dropdown from selenium.webdriver.support.select import Select dropdown = Select(driver.find_element(By.ID, "deviceType")) dropdown.select_by_visible_text("Router") # Click submit submit_btn = driver.find_element(By.ID, "btnSave") submit_btn.click() time.sleep(2) # Verify submission assert "success" in driver.page_source.lower() ``` ### 5. AJAX/Fetch Tests ```python def test_ajax_request(driver): """Test: AJAX request completes successfully""" driver.get(f"{BASE_URL}/page.php") time.sleep(2) # Click button that triggers AJAX ajax_btn = driver.find_element(By.ID, "loadData") ajax_btn.click() # Wait for AJAX to complete (look for loading indicator to disappear) WebDriverWait(driver, 10).until( EC.invisibility_of_element((By.CLASS_NAME, "spinner")) ) # Verify data loaded data_table = driver.find_element(By.ID, "dataTable") assert len(data_table.text) > 0, "Data should be loaded" ``` ### 6. API Endpoint Tests ```python def test_api_endpoint(api_token): """Test: API endpoint returns correct data""" response = api_get("/devices", api_token) assert response.status_code == 200 data = response.json() assert data["success"] == True assert len(data["results"]) > 0 ``` ### 7. Multi-Step Workflow Tests ```python def test_device_edit_workflow(driver): """Test: Complete device edit workflow""" # Step 1: Navigate to devices page driver.get(f"{BASE_URL}/devices.php") time.sleep(2) # Step 2: Click first device first_device = driver.find_element(By.CSS_SELECTOR, "table tbody tr:first-child a") first_device.click() time.sleep(2) # Step 3: Edit device name name_field = driver.find_element(By.ID, "deviceName") original_name = name_field.get_attribute("value") name_field.clear() name_field.send_keys("Updated Name") # Step 4: Save changes save_btn = driver.find_element(By.ID, "btnSave") save_btn.click() time.sleep(2) # Step 5: Verify save succeeded assert "success" in driver.page_source.lower() # Step 6: Restore original name name_field = driver.find_element(By.ID, "deviceName") name_field.clear() name_field.send_keys(original_name) save_btn = driver.find_element(By.ID, "btnSave") save_btn.click() ``` ## Common Selenium Patterns ### Finding Elements ```python # By ID (fastest, most reliable) element = driver.find_element(By.ID, "myButton") # By CSS selector (flexible) element = driver.find_element(By.CSS_SELECTOR, ".btn-primary") elements = driver.find_elements(By.CSS_SELECTOR, "table tr") # By XPath (powerful but slow) element = driver.find_element(By.XPATH, "//button[@type='submit']") # By link text element = driver.find_element(By.LINK_TEXT, "Edit Device") # By partial link text element = driver.find_element(By.PARTIAL_LINK_TEXT, "Edit") # Check if element exists (don't fail if missing) elements = driver.find_elements(By.ID, "optional_element") if len(elements) > 0: elements[0].click() ``` ### Waiting for Elements ```python from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # Wait up to 10 seconds for element to be present element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "myElement")) ) # Wait for element to be clickable element = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "myButton")) ) # Wait for element to disappear WebDriverWait(driver, 10).until( EC.invisibility_of_element((By.CLASS_NAME, "loading-spinner")) ) # Wait for text to be present WebDriverWait(driver, 10).until( EC.text_to_be_present_in_element((By.ID, "status"), "Complete") ) ``` ### Interacting with Elements ```python # Click button.click() # Type text input_field.send_keys("Hello World") # Clear and type input_field.clear() input_field.send_keys("New Text") # Get text text = element.text # Get attribute value = input_field.get_attribute("value") href = link.get_attribute("href") # Check visibility if element.is_displayed(): element.click() # Check if enabled if button.is_enabled(): button.click() # Check if selected (checkboxes/radio) if checkbox.is_selected(): checkbox.click() # Uncheck it ``` ### Handling Alerts/Modals ```python # Wait for alert WebDriverWait(driver, 5).until(EC.alert_is_present()) # Accept alert (click OK) alert = driver.switch_to.alert alert.accept() # Dismiss alert (click Cancel) alert.dismiss() # Get alert text alert_text = alert.text # Bootstrap modals modal = driver.find_element(By.ID, "myModal") assert modal.is_displayed(), "Modal should be visible" ``` ### Handling Dropdowns ```python from selenium.webdriver.support.select import Select # Select by visible text dropdown = Select(driver.find_element(By.ID, "myDropdown")) dropdown.select_by_visible_text("Option 1") # Select by value dropdown.select_by_value("option1") # Select by index dropdown.select_by_index(0) # Get selected option selected = dropdown.first_selected_option print(selected.text) # Get all options all_options = dropdown.options for option in all_options: print(option.text) ``` ## Running Tests ### Run all tests ```bash pytest test/ui/ ``` ### Run specific test file ```bash pytest test/ui/test_ui_dashboard.py ``` ### Run specific test ```bash pytest test/ui/test_ui_dashboard.py::test_dashboard_loads ``` ### Run with verbose output ```bash pytest test/ui/ -v ``` ### Run with very verbose output (show page source on failures) ```bash pytest test/ui/ -vv ``` ### Run and stop on first failure ```bash pytest test/ui/ -x ``` ## Best Practices 1. **Use explicit waits** instead of `time.sleep()` when possible 2. **Test the behavior, not implementation** - focus on what users see/do 3. **Keep tests independent** - each test should work alone 4. **Clean up after tests** - reset any changes made during testing 5. **Use descriptive test names** - `test_export_csv_button_downloads_file` not `test_1` 6. **Add docstrings** - explain what each test validates 7. **Test error cases** - not just happy paths 8. **Use CSS selectors over XPath** when possible (faster, more readable) 9. **Group related tests** - keep page-specific tests in same file 10. **Avoid hardcoded waits** - use WebDriverWait with conditions ## Debugging Failed Tests ### Take screenshot on failure ```python try: assert something except AssertionError: driver.save_screenshot("/tmp/test_failure.png") raise ``` ### Print page source ```python print(driver.page_source) ``` ### Print current URL ```python print(driver.current_url) ``` ### Check console logs (JavaScript errors) ```python logs = driver.get_log('browser') for log in logs: print(log) ``` ### Run in non-headless mode (see what's happening) Modify `test_helpers.py`: ```python # Comment out this line: # chrome_options.add_argument('--headless=new') ``` ## Example: Complete Functional Test ```python def test_device_delete_workflow(driver, api_token): """Test: Complete device deletion workflow""" # Setup: Create a test device via API import requests headers = {"Authorization": f"Bearer {api_token}"} test_device = { "mac": "00:11:22:33:44:55", "name": "Test Device", "type": "Other" } create_response = requests.post( f"{API_BASE_URL}/device", headers=headers, json=test_device ) assert create_response.status_code == 200 # Navigate to devices page driver.get(f"{BASE_URL}/devices.php") time.sleep(2) # Search for the test device search_box = driver.find_element(By.CSS_SELECTOR, ".dataTables_filter input") search_box.send_keys("Test Device") time.sleep(1) # Click delete button for the device delete_btn = driver.find_element(By.CSS_SELECTOR, "button.btn-delete") delete_btn.click() # Confirm deletion in modal time.sleep(0.5) confirm_btn = driver.find_element(By.ID, "btnConfirmDelete") confirm_btn.click() # Wait for success message WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "alert-success")) ) # Verify device is gone via API verify_response = requests.get( f"{API_BASE_URL}/device/00:11:22:33:44:55", headers=headers ) assert verify_response.status_code == 404, "Device should be deleted" ``` ## Settings Form Submission Tests The `test_ui_settings.py` file includes tests for validating the settings save workflow via PHP form submission: ### `test_save_settings_with_form_submission(driver)` Tests that the settings form submits correctly to `php/server/util.php` with `function: 'savesettings'`. Validates that the config file is generated correctly and no errors appear on save. ### `test_save_settings_no_loss_of_data(driver)` Verifies that all settings are preserved when saved (no data loss during save operation). **Key Coverage**: Form submission flow → PHP `saveSettings()` → Config file generation with Python-compatible formatting