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

12 KiB

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)

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

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)

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

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

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

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

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

# 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

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

# 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

# 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

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

pytest test/ui/

Run specific test file

pytest test/ui/test_ui_dashboard.py

Run specific test

pytest test/ui/test_ui_dashboard.py::test_dashboard_loads

Run with verbose output

pytest test/ui/ -v

Run with very verbose output (show page source on failures)

pytest test/ui/ -vv

Run and stop on first failure

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

try:
    assert something
except AssertionError:
    driver.save_screenshot("/tmp/test_failure.png")
    raise

Print page source

print(driver.page_source)

Print current URL

print(driver.current_url)

Check console logs (JavaScript errors)

logs = driver.get_log('browser')
for log in logs:
    print(log)

Run in non-headless mode (see what's happening)

Modify test_helpers.py:

# Comment out this line:
# chrome_options.add_argument('--headless=new')

Example: Complete Functional Test

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):

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.