Files
NetAlertX/test/ui/TESTING_GUIDE.md
T
Alexander Shevchenko 1aceeae218 test(frontend): cover network topology rendering and interactions
Add a standalone fixture using the real topology renderer and styles.
Check painted pixels in WebKit, Chromium and Firefox because DOM bounds
can remain correct when WebKit paints foreignObject content incorrectly.

Exercise node clicks, collapse/expand, wheel zoom and drag pan, including
assertions that pointer gestures change the SVG transform.

Document dependency installation and execution separately from pytest.
The regression fails with the original frontend and passes with the fix.
2026-10-04 16:45:29 +02:00

437 lines
12 KiB
Markdown

# 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
# Network topology rendering in WebKit, Chromium and Firefox
The standalone regression uses the real Treeviz bundle, network renderer and styles
with four fixture devices. No running NetAlertX backend or database is required.
It checks painted pixels, node clicks, collapse/expand, wheel zoom and drag pan.
Pixel checks are necessary because WebKit may return correct DOM bounds while
painting positioned or translucent HTML at the SVG origin instead.
Run from the repository root (Node.js required):
```sh
npm install --prefix /tmp/netalertx-browser-tests playwright pngjs
/tmp/netalertx-browser-tests/node_modules/.bin/playwright install --with-deps webkit chromium firefox
NODE_PATH=/tmp/netalertx-browser-tests/node_modules node test/ui/test_network_topology.cjs
```
Screenshots are written to `/tmp/netalertx-topology`, or to the directory specified
by `NETALERTX_TOPOLOGY_ARTIFACTS`. This regression runs separately from pytest.
Also verify the fixture in native Safari when a Mac is available; Playwright
WebKit is useful regression coverage but is not the Safari application.