Files
led-controller/tests/test_browser.py
2026-04-06 18:22:03 +12:00

1042 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Browser automation tests using Selenium.
Tests run against the device at 192.168.4.1 in an actual browser.
On Pi OS Lite (no desktop) these tests are skipped unless headless Chromium
and chromedriver are installed (e.g. chromium-browser chromium-chromedriver).
"""
import os
import pytest
if os.environ.get("LED_CONTROLLER_RUN_BROWSER_TESTS") != "1":
pytest.skip(
"Legacy device browser automation script; enable explicitly to run.",
allow_module_level=True,
)
import sys
import time
import requests
from typing import Optional, List
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import TimeoutException, NoSuchElementException
# Base URL for the device
BASE_URL = "http://192.168.4.1"
class BrowserTest:
"""Browser automation test class."""
def __init__(self, base_url: str = BASE_URL, headless: bool = False):
self.base_url = base_url
self.driver = None
self.headless = headless
self.created_tabs: List[str] = []
self.created_profiles: List[str] = []
self.created_presets: List[str] = []
def setup(self):
"""Set up the browser driver. Tries Chrome first, then Firefox."""
err_chrome, err_firefox = None, None
# Try Chrome first
try:
opts = ChromeOptions()
if self.headless:
opts.add_argument('--headless')
opts.add_argument('--no-sandbox')
opts.add_argument('--disable-dev-shm-usage')
opts.add_argument('--disable-gpu')
opts.add_argument('--window-size=1920,1080')
self.driver = webdriver.Chrome(options=opts)
self.driver.implicitly_wait(5)
print("✓ Browser started (Chrome)")
return True
except Exception as e:
err_chrome = e
# Fallback to Firefox
try:
opts = FirefoxOptions()
if self.headless:
opts.add_argument('--headless')
self.driver = webdriver.Firefox(options=opts)
self.driver.implicitly_wait(5)
print("✓ Browser started (Firefox)")
return True
except Exception as e:
err_firefox = e
print("✗ Failed to start browser.")
if err_chrome:
print(f" Chrome: {err_chrome}")
if err_firefox:
print(f" Firefox: {err_firefox}")
print(" On Raspberry Pi (aarch64), install: chromium-browser and chromium-chromedriver")
return False
def teardown(self):
"""Close the browser."""
if self.driver:
self.driver.quit()
print("✓ Browser closed")
def navigate(self, path: str = '/'):
"""Navigate to a URL."""
url = f"{self.base_url}{path}"
try:
self.driver.get(url)
time.sleep(1) # Wait for page load
return True
except Exception as e:
print(f"✗ Failed to navigate to {url}: {e}")
return False
def wait_for_element(self, by, value, timeout=10):
"""Wait for an element to appear."""
try:
element = WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located((by, value))
)
return element
except TimeoutException:
return None
def click_element(self, by, value, timeout=10, use_js=False):
"""Click an element."""
try:
element = self.wait_for_element(by, value, timeout)
if element:
if use_js:
# Use JavaScript click for elements that might be intercepted
self.driver.execute_script("arguments[0].click();", element)
else:
# Try normal click first, fall back to JS if it fails
try:
element.click()
except Exception:
self.driver.execute_script("arguments[0].click();", element)
time.sleep(0.5) # Wait for action
return True
return False
except Exception as e:
print(f"✗ Failed to click {value}: {e}")
return False
def handle_alert(self, accept=True, timeout=2):
"""Handle an alert dialog."""
try:
alert = WebDriverWait(self.driver, timeout).until(EC.alert_is_present())
if accept:
alert.accept()
else:
alert.dismiss()
time.sleep(0.3)
return True
except TimeoutException:
return False
except Exception as e:
print(f"✗ Failed to handle alert: {e}")
return False
def cleanup_test_data(self):
"""Clean up test data created during tests."""
print("\n Cleaning up test data...")
try:
# Use requests to make API calls for cleanup
session = requests.Session()
# Delete created presets by ID
for preset_id in self.created_presets:
try:
response = session.delete(f"{self.base_url}/presets/{preset_id}")
if response.status_code == 200:
print(f" ✓ Cleaned up preset: {preset_id}")
except Exception as e:
print(f" ⚠ Failed to cleanup preset {preset_id}: {e}")
# Delete created tabs by ID
for zone_id in self.created_tabs:
try:
response = session.delete(f"{self.base_url}/zones/{zone_id}")
if response.status_code == 200:
print(f" ✓ Cleaned up zone: {zone_id}")
except Exception as e:
print(f" ⚠ Failed to cleanup zone {zone_id}: {e}")
# Delete created profiles by ID
for profile_id in self.created_profiles:
try:
response = session.delete(f"{self.base_url}/profiles/{profile_id}")
if response.status_code == 200:
print(f" ✓ Cleaned up profile: {profile_id}")
except Exception as e:
print(f" ⚠ Failed to cleanup profile {profile_id}: {e}")
# Also try to cleanup by name pattern (in case IDs weren't tracked)
test_names = ['Browser Test Zone', 'Browser Test Profile', 'Browser Test Preset',
'Preset 1', 'Preset 2', 'Preset 3', 'Edited Browser Zone']
# Cleanup tabs by name
try:
tabs_response = session.get(f"{self.base_url}/zones")
if tabs_response.status_code == 200:
tabs_data = tabs_response.json()
tabs = tabs_data.get('zones', {})
for zone_id, tab_data in zones.items():
if isinstance(tab_data, dict) and tab_data.get('name') in test_names:
try:
session.delete(f"{self.base_url}/zones/{zone_id}")
print(f" ✓ Cleaned up zone by name: {tab_data.get('name')}")
except:
pass
except:
pass
# Cleanup profiles by name
try:
profiles_response = session.get(f"{self.base_url}/profiles")
if profiles_response.status_code == 200:
profiles_data = profiles_response.json()
profiles = profiles_data.get('profiles', {})
for profile_id, profile_data in profiles.items():
if isinstance(profile_data, dict) and profile_data.get('name') in test_names:
try:
session.delete(f"{self.base_url}/profiles/{profile_id}")
print(f" ✓ Cleaned up profile by name: {profile_data.get('name')}")
except:
pass
except:
pass
# Cleanup presets by name
try:
presets_response = session.get(f"{self.base_url}/presets")
if presets_response.status_code == 200:
presets_data = presets_response.json()
presets = presets_data.get('presets', {}) if isinstance(presets_data, dict) else presets_data
for preset_id, preset_data in presets.items():
if isinstance(preset_data, dict) and preset_data.get('name') in test_names:
try:
session.delete(f"{self.base_url}/presets/{preset_id}")
print(f" ✓ Cleaned up preset by name: {preset_data.get('name')}")
except:
pass
except:
pass
# Clear the lists
self.created_tabs.clear()
self.created_profiles.clear()
self.created_presets.clear()
except Exception as e:
print(f" ⚠ Cleanup error: {e}")
def fill_input(self, by, value, text, timeout=10):
"""Fill an input field."""
try:
element = self.wait_for_element(by, value, timeout)
if element:
element.clear()
element.send_keys(text)
return True
return False
except Exception as e:
print(f"✗ Failed to fill {value}: {e}")
return False
def get_text(self, by, value, timeout=10):
"""Get text from an element."""
try:
element = self.wait_for_element(by, value, timeout)
if element:
return element.text
return None
except Exception as e:
return None
def get_cookie(self, name: str):
"""Get a cookie value."""
try:
return self.driver.get_cookie(name)
except:
return None
def drag_and_drop(self, source_element, target_element):
"""Perform drag and drop operation."""
try:
actions = ActionChains(self.driver)
actions.drag_and_drop(source_element, target_element).perform()
time.sleep(0.5) # Wait for drop to complete
return True
except Exception as e:
print(f"✗ Drag and drop failed: {e}")
return False
def drag_and_drop_by_offset(self, element, x_offset, y_offset):
"""Perform drag and drop by offset."""
try:
actions = ActionChains(self.driver)
actions.drag_and_drop_by_offset(element, x_offset, y_offset).perform()
time.sleep(0.5)
return True
except Exception as e:
print(f"✗ Drag and drop by offset failed: {e}")
return False
def test_browser_connection(browser: BrowserTest) -> bool:
"""Test basic browser connection."""
print("Testing browser connection...")
if not browser.setup():
return False
try:
if browser.navigate('/'):
print("✓ Successfully loaded page")
title = browser.driver.title
print(f" Page title: {title}")
return True
else:
print("✗ Failed to load page")
return False
finally:
browser.teardown()
def test_tabs_ui(browser: BrowserTest) -> bool:
"""Test tabs UI in browser."""
print("\n=== Testing Tabs UI in Browser ===")
passed = 0
total = 0
if not browser.setup():
return False
try:
# Test 1: Load page
total += 1
if browser.navigate('/'):
print("✓ Loaded main page")
passed += 1
else:
print("✗ Failed to load main page")
browser.teardown()
return False
# Test 2: Open tabs modal
total += 1
if browser.click_element(By.ID, 'zones-btn'):
print("✓ Clicked Tabs button")
# Wait for modal to appear
time.sleep(0.5)
modal = browser.wait_for_element(By.ID, 'zones-modal')
if modal and 'active' in modal.get_attribute('class'):
print("✓ Tabs modal opened")
passed += 1
else:
print("✗ Tabs modal didn't open")
else:
print("✗ Failed to click Tabs button")
# Test 3: Create a zone via UI
total += 1
try:
# Fill in zone name
if browser.fill_input(By.ID, 'new-zone-name', 'Browser Test Zone'):
print(" ✓ Filled zone name")
# Devices default from registry or placeholder name "1"
# Click create button
if browser.click_element(By.ID, 'create-zone-btn'):
print(" ✓ Clicked create button")
time.sleep(1) # Wait for creation
# Check if zone appears in list and extract ID
tabs_list = browser.wait_for_element(By.ID, 'zones-list-modal')
if tabs_list:
list_text = tabs_list.text
if 'Browser Test Zone' in list_text:
print("✓ Created zone via UI")
# Try to extract zone ID from the list (look for data-zone-id attribute)
try:
tab_rows = browser.driver.find_elements(By.CSS_SELECTOR, '#zones-list-modal .profiles-row')
for row in tab_rows:
if 'Browser Test Zone' in row.text:
zone_id = row.get_attribute('data-zone-id')
if zone_id:
browser.created_tabs.append(zone_id)
break
except:
pass # If we can't extract ID, cleanup will try by name
passed += 1
else:
print("✗ Zone not found in list after creation")
else:
print("✗ Tabs list not found")
else:
print("✗ Failed to click create button")
except Exception as e:
print(f"✗ Failed to create zone via UI: {e}")
# Test 4: Edit a zone via UI (right-click in Tabs list)
total += 1
try:
# First, close and reopen modal to refresh
browser.click_element(By.ID, 'zones-close-btn')
time.sleep(0.5)
browser.click_element(By.ID, 'zones-btn')
time.sleep(0.5)
# Right-click the row corresponding to 'Browser Test Zone'
try:
tab_row = browser.driver.find_element(
By.XPATH,
"//div[@id='zones-list-modal']//div[contains(@class,'profiles-row')][.//span[contains(text(), 'Browser Test Zone')]]"
)
except Exception:
tab_row = None
if tab_row:
actions = ActionChains(browser.driver)
actions.context_click(tab_row).perform()
time.sleep(0.5)
# Check if edit modal opened
edit_modal = browser.wait_for_element(By.ID, 'edit-zone-modal')
if edit_modal:
print("✓ Edit modal opened via right-click")
# Fill in new name
if browser.fill_input(By.ID, 'edit-zone-name', 'Edited Browser Zone'):
print(" ✓ Filled new zone name")
# Submit form
edit_form = browser.wait_for_element(By.ID, 'edit-zone-form')
if edit_form:
browser.driver.execute_script("arguments[0].submit();", edit_form)
time.sleep(1) # Wait for update
print("✓ Submitted edit form")
passed += 1
else:
print("✗ Edit form not found")
else:
print("✗ Edit modal didn't open after right-click")
else:
print("✗ Could not find zone row for 'Browser Test Zone'")
except Exception as e:
print(f"✗ Failed to edit zone via UI: {e}")
import traceback
traceback.print_exc()
# Test 5: Check current zone cookie
total += 1
cookie = browser.get_cookie('current_zone')
if cookie:
print(f"✓ Found current_zone cookie: {cookie.get('value')}")
passed += 1
else:
print("⚠ No current_zone cookie found (might be normal if no zone selected)")
passed += 1 # Not a failure, just informational
# Close modal
browser.click_element(By.ID, 'zones-close-btn')
except Exception as e:
print(f"✗ Browser test error: {e}")
import traceback
traceback.print_exc()
finally:
browser.cleanup_test_data()
browser.teardown()
print(f"\nBrowser tabs UI tests: {passed}/{total} passed")
return passed == total
def test_profiles_ui(browser: BrowserTest) -> bool:
"""Test profiles UI in browser."""
print("\n=== Testing Profiles UI in Browser ===")
passed = 0
total = 0
if not browser.setup():
return False
try:
# Test 1: Load page
total += 1
if browser.navigate('/'):
print("✓ Loaded main page")
passed += 1
else:
browser.teardown()
return False
# Test 2: Open profiles modal
total += 1
if browser.click_element(By.ID, 'profiles-btn'):
print("✓ Clicked Profiles button")
time.sleep(0.5)
modal = browser.wait_for_element(By.ID, 'profiles-modal')
if modal:
print("✓ Profiles modal opened")
passed += 1
else:
print("✗ Profiles modal didn't open")
# Test 3: Create profile via UI
total += 1
try:
if browser.fill_input(By.ID, 'new-profile-name', 'Browser Test Profile'):
print(" ✓ Filled profile name")
if browser.click_element(By.ID, 'create-profile-btn'):
print(" ✓ Clicked create button")
time.sleep(1)
# Check if profile appears
profiles_list = browser.wait_for_element(By.ID, 'profiles-list')
if profiles_list and 'Browser Test Profile' in profiles_list.text:
print("✓ Created profile via UI")
passed += 1
else:
print("✗ Profile not found in list")
except Exception as e:
print(f"✗ Failed to create profile: {e}")
# Close modal
browser.click_element(By.ID, 'profiles-close-btn')
except Exception as e:
print(f"✗ Browser test error: {e}")
import traceback
traceback.print_exc()
finally:
browser.cleanup_test_data()
browser.teardown()
print(f"\nBrowser profiles UI tests: {passed}/{total} passed")
return passed == total
def test_mobile_tab_presets_two_columns():
"""
Verify that the zone preset selecting area shows roughly two preset tiles per row
on a phone-sized viewport.
"""
bt = BrowserTest(base_url=BASE_URL, headless=True)
if not bt.setup():
assert False, "Failed to start browser"
try:
# Simulate a mobile viewport
bt.driver.set_window_size(400, 800)
assert bt.navigate('/'), "Failed to load main page"
# Click the first zone button to load presets for that zone
first_tab = bt.wait_for_element(By.CSS_SELECTOR, '.zone-button', timeout=10)
assert first_tab is not None, "No zone buttons found"
first_tab.click()
time.sleep(1)
container = bt.wait_for_element(By.ID, 'presets-list-zone', timeout=10)
assert container is not None, "presets-list-zone not found"
tiles = bt.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .preset-tile-row')
# Need at least 2 presets to make this meaningful
assert len(tiles) >= 2, "Fewer than 2 presets found for zone"
container_width = container.size['width']
first_width = tiles[0].size['width']
# Each tile should be about half the container width (tolerate some margin)
assert 0.4 * container_width <= first_width <= 0.6 * container_width, (
f"Preset tile width {first_width} not ~half of container {container_width}"
)
finally:
bt.teardown()
def test_presets_ui(browser: BrowserTest) -> bool:
"""Test presets UI in browser."""
print("\n=== Testing Presets UI in Browser ===")
passed = 0
total = 0
if not browser.setup():
return False
try:
# Test 1: Load page
total += 1
if browser.navigate('/'):
print("✓ Loaded main page")
passed += 1
else:
browser.teardown()
return False
# Test 2: Open presets modal
total += 1
if browser.click_element(By.ID, 'presets-btn'):
print("✓ Clicked Presets button")
time.sleep(0.5)
modal = browser.wait_for_element(By.ID, 'presets-modal')
if modal:
print("✓ Presets modal opened")
passed += 1
else:
print("✗ Presets modal didn't open")
# Test 3: Click Add button to open preset editor
total += 1
try:
if browser.click_element(By.ID, 'preset-add-btn'):
print(" ✓ Clicked Add Preset button")
time.sleep(0.5)
editor_modal = browser.wait_for_element(By.ID, 'preset-editor-modal')
if editor_modal:
print("✓ Preset editor modal opened")
passed += 1
else:
print("✗ Preset editor modal didn't open")
else:
print("✗ Failed to click Add Preset button")
except Exception as e:
print(f"✗ Failed to open preset editor: {e}")
# Test 4: Create a preset via UI
total += 1
try:
# Fill preset name
if browser.fill_input(By.ID, 'preset-name-input', 'Browser Test Preset'):
print(" ✓ Filled preset name")
# Select a pattern
pattern_select = browser.wait_for_element(By.ID, 'preset-pattern-input')
if pattern_select:
# Try to select first available pattern
from selenium.webdriver.support.ui import Select
select = Select(pattern_select)
if len(select.options) > 1: # More than just the placeholder
select.select_by_index(1) # Select first real pattern
print(" ✓ Selected pattern")
# Add a color
if browser.fill_input(By.ID, 'preset-new-color', '#ff0000'):
print(" ✓ Set color")
if browser.click_element(By.ID, 'preset-add-color-btn'):
print(" ✓ Added color")
# Set brightness
if browser.fill_input(By.ID, 'preset-brightness-input', '200'):
print(" ✓ Set brightness")
# Save preset
if browser.click_element(By.ID, 'preset-save-btn'):
print(" ✓ Clicked save button")
time.sleep(1)
# Check if preset appears in list
presets_list = browser.wait_for_element(By.ID, 'presets-list')
if presets_list and 'Browser Test Preset' in presets_list.text:
print("✓ Created preset via UI")
passed += 1
else:
print("✗ Preset not found in list after creation")
else:
print("✗ Failed to click save button")
except Exception as e:
print(f"✗ Failed to create preset: {e}")
import traceback
traceback.print_exc()
# Close editor modal
browser.click_element(By.ID, 'preset-editor-close-btn', use_js=True)
time.sleep(0.5)
# Close presets modal
browser.click_element(By.ID, 'presets-close-btn', use_js=True)
except Exception as e:
print(f"✗ Browser test error: {e}")
import traceback
traceback.print_exc()
finally:
browser.cleanup_test_data()
browser.teardown()
print(f"\nBrowser presets UI tests: {passed}/{total} passed")
return passed == total
def test_color_palette_ui(browser: BrowserTest) -> bool:
"""Test color palette UI in browser."""
print("\n=== Testing Color Palette UI in Browser ===")
passed = 0
total = 0
if not browser.setup():
return False
try:
# Test 1: Load page
total += 1
if browser.navigate('/'):
print("✓ Loaded main page")
passed += 1
else:
browser.teardown()
return False
# Test 2: Open color palette modal
total += 1
if browser.click_element(By.ID, 'color-palette-btn'):
print("✓ Clicked Color Palette button")
time.sleep(0.5)
modal = browser.wait_for_element(By.ID, 'color-palette-modal')
if modal:
print("✓ Color palette modal opened")
passed += 1
else:
print("✗ Color palette modal didn't open")
# Test 3: Add a color to palette
total += 1
try:
# Set color picker - use a unique color that's likely not in palette
color_input = browser.wait_for_element(By.ID, 'palette-new-color')
if color_input:
# Use a unique color (purple) that's less likely to be in palette
browser.driver.execute_script("arguments[0].value = '#9b59b6'; arguments[0].dispatchEvent(new Event('input'));", color_input)
print(" ✓ Set color to #9b59b6")
# Click add color button
if browser.click_element(By.ID, 'palette-add-color-btn'):
print(" ✓ Clicked Add Color button")
time.sleep(0.5)
# Handle alert if color already exists
browser.handle_alert(accept=True, timeout=1)
# Check if color appears in palette
palette_container = browser.wait_for_element(By.ID, 'palette-container')
if palette_container:
# Look for color swatches
color_swatches = browser.driver.find_elements(By.CSS_SELECTOR, '#palette-container .draggable-color-swatch, #palette-container [style*="background-color"]')
if color_swatches:
print("✓ Added color to palette")
passed += 1
else:
print("✗ Color not found in palette after adding")
else:
print("✗ Palette container not found")
else:
print("✗ Failed to click Add Color button")
except Exception as e:
print(f"✗ Failed to add color: {e}")
import traceback
traceback.print_exc()
# Test 4: Test drag and drop colors (if multiple colors exist)
total += 1
try:
color_swatches = browser.driver.find_elements(By.CSS_SELECTOR, '#palette-container .draggable-color-swatch, #palette-container [draggable="true"]')
if len(color_swatches) >= 2:
# Drag first color to second position
source = color_swatches[0]
target = color_swatches[1]
if browser.drag_and_drop(source, target):
print("✓ Dragged color to reorder")
time.sleep(0.5)
passed += 1
else:
print("✗ Drag and drop failed")
else:
print("⚠ Not enough colors to test drag and drop (need at least 2)")
passed += 1 # Not a failure
except Exception as e:
print(f"✗ Drag and drop test error: {e}")
# Close modal
browser.click_element(By.ID, 'color-palette-close-btn', use_js=True)
except Exception as e:
print(f"✗ Browser test error: {e}")
import traceback
traceback.print_exc()
finally:
browser.cleanup_test_data()
browser.teardown()
print(f"\nBrowser color palette UI tests: {passed}/{total} passed")
return passed >= total - 1 # Allow one failure (alert handling might be flaky)
def test_preset_drag_and_drop(browser: BrowserTest) -> bool:
"""Test dragging presets around in a zone."""
print("\n=== Testing Preset Drag and Drop in Zone ===")
passed = 0
total = 0
if not browser.setup():
return False
try:
# Test 1: Load page and ensure we have a zone
total += 1
if browser.navigate('/'):
print("✓ Loaded main page")
passed += 1
else:
browser.teardown()
return False
# Test 2: Open tabs modal and create/select a zone
total += 1
browser.click_element(By.ID, 'zones-btn')
time.sleep(0.5)
# Check if we have tabs, if not create one
tabs_list = browser.wait_for_element(By.ID, 'zones-list-modal')
if tabs_list and 'No tabs found' in tabs_list.text:
# Create a zone
browser.fill_input(By.ID, 'new-zone-name', 'Drag Test Zone')
browser.click_element(By.ID, 'create-zone-btn')
time.sleep(1)
# Select first zone (or the one we just created)
select_buttons = browser.driver.find_elements(By.XPATH, "//button[contains(text(), 'Select')]")
if select_buttons:
select_buttons[0].click()
time.sleep(1)
print("✓ Selected a zone")
passed += 1
else:
print("✗ No tabs available to select")
browser.click_element(By.ID, 'zones-close-btn')
browser.teardown()
return False
browser.click_element(By.ID, 'zones-close-btn', use_js=True)
time.sleep(0.5)
# Test 3: Open presets modal and create presets
total += 1
browser.click_element(By.ID, 'presets-btn')
time.sleep(0.5)
# Create first preset
browser.click_element(By.ID, 'preset-add-btn')
time.sleep(0.5)
browser.fill_input(By.ID, 'preset-name-input', 'Preset 1')
browser.fill_input(By.ID, 'preset-new-color', '#ff0000')
browser.click_element(By.ID, 'preset-add-color-btn')
browser.click_element(By.ID, 'preset-save-btn')
time.sleep(1)
# Create second preset
browser.click_element(By.ID, 'preset-add-btn')
time.sleep(0.5)
browser.fill_input(By.ID, 'preset-name-input', 'Preset 2')
browser.fill_input(By.ID, 'preset-new-color', '#00ff00')
browser.click_element(By.ID, 'preset-add-color-btn')
browser.click_element(By.ID, 'preset-save-btn')
time.sleep(1)
# Create third preset
browser.click_element(By.ID, 'preset-add-btn')
time.sleep(0.5)
browser.fill_input(By.ID, 'preset-name-input', 'Preset 3')
browser.fill_input(By.ID, 'preset-new-color', '#0000ff')
browser.click_element(By.ID, 'preset-add-color-btn')
browser.click_element(By.ID, 'preset-save-btn')
time.sleep(1)
browser.click_element(By.ID, 'presets-close-btn', use_js=True)
time.sleep(0.5)
print("✓ Created 3 presets for drag test")
passed += 1
# Test 4: Add presets to the zone (via Edit Zone modal Add buttons in list)
total += 1
try:
zone_id = browser.driver.execute_script(
"return (window.tabsManager && window.tabsManager.getCurrentTabId && window.tabsManager.getCurrentTabId()) || null;"
)
if not zone_id:
print("✗ Could not get current zone id")
else:
browser.driver.execute_script(
"if (window.tabsManager && window.tabsManager.openEditTabModal) { window.tabsManager.openEditTabModal(arguments[0], null); }",
zone_id
)
time.sleep(1)
list_el = browser.wait_for_element(By.ID, 'edit-zone-presets-list', timeout=5)
if list_el:
select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']")
if len(select_buttons) >= 2:
browser.driver.execute_script("arguments[0].click();", select_buttons[0])
time.sleep(1.5)
browser.handle_alert(accept=True, timeout=1)
select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']")
if len(select_buttons) >= 1:
browser.driver.execute_script("arguments[0].click();", select_buttons[0])
time.sleep(1.5)
browser.handle_alert(accept=True, timeout=1)
print(" ✓ Added 2 presets to zone")
passed += 1
elif len(select_buttons) == 1:
browser.driver.execute_script("arguments[0].click();", select_buttons[0])
time.sleep(1.5)
browser.handle_alert(accept=True, timeout=1)
print(" ✓ Added 1 preset to zone")
passed += 1
else:
print(" ⚠ No presets available to add (all already in zone)")
else:
print("✗ Edit zone presets list not found")
except Exception as e:
print(f"✗ Failed to add presets to zone: {e}")
import traceback
traceback.print_exc()
# Test 5: Find presets in zone and test drag and drop (Edit mode only)
total += 1
try:
# Wait for presets to load in the zone
presets_list_tab = browser.wait_for_element(By.ID, 'presets-list-zone', timeout=5)
if presets_list_tab:
time.sleep(1) # Wait for presets to render
# Reordering is only available in Edit mode (tiles get .draggable-preset)
mode_toggle = browser.wait_for_element(By.CSS_SELECTOR, '.ui-mode-toggle', timeout=5)
if mode_toggle and mode_toggle.get_attribute('aria-pressed') == 'false':
mode_toggle.click()
time.sleep(0.5)
# Find draggable preset elements - wait a bit more for rendering
time.sleep(1)
draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset')
if len(draggable_presets) >= 2:
print(f" ✓ Found {len(draggable_presets)} draggable presets")
# Get initial order
initial_order = [p.text for p in draggable_presets]
print(f" Initial order: {initial_order[:3]}") # Show first 3
# Drag first preset down
source = draggable_presets[0]
target = draggable_presets[1]
# Use ActionChains for drag and drop
actions = ActionChains(browser.driver)
actions.click_and_hold(source).move_to_element(target).release().perform()
time.sleep(1) # Wait for reorder to complete
# Check if order changed
draggable_presets_after = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset')
if len(draggable_presets_after) >= 2:
new_order = [p.text for p in draggable_presets_after]
print(f" New order: {new_order[:3]}")
if initial_order != new_order:
print("✓ Preset drag and drop worked - order changed")
passed += 1
else:
print("⚠ Order didn't change (might need to verify with API)")
passed += 1 # Not necessarily a failure
else:
print("✗ Presets disappeared after drag")
elif len(draggable_presets) == 1:
print(f"⚠ Only 1 preset found in zone (need 2 for drag test). Preset: {draggable_presets[0].text}")
zone_id = browser.driver.execute_script(
"return (window.tabsManager && window.tabsManager.getCurrentTabId && window.tabsManager.getCurrentTabId()) || null;"
)
if zone_id:
browser.driver.execute_script(
"if (window.tabsManager && window.tabsManager.openEditTabModal) { window.tabsManager.openEditTabModal(arguments[0], null); }",
zone_id
)
time.sleep(1)
select_buttons = browser.driver.find_elements(By.XPATH, "//div[@id='edit-zone-presets-list']//button[text()='Add']")
if select_buttons:
print(" Attempting to add another preset...")
browser.driver.execute_script("arguments[0].click();", select_buttons[0])
time.sleep(1.5)
browser.handle_alert(accept=True, timeout=1)
try:
browser.driver.execute_script("document.getElementById('edit-zone-modal').classList.remove('active');")
except Exception:
pass
time.sleep(1)
draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-zone .draggable-preset')
if len(draggable_presets) >= 2:
print(" ✓ Added another preset, now testing drag...")
source = draggable_presets[0]
target = draggable_presets[1]
actions = ActionChains(browser.driver)
actions.click_and_hold(source).move_to_element(target).release().perform()
time.sleep(1)
print("✓ Performed drag and drop")
passed += 1
else:
print(f" ✗ Still only {len(draggable_presets)} preset(s) after adding")
else:
print(" ✗ No Add buttons found in Edit Zone modal")
else:
print(f"✗ No presets found in zone (found {len(draggable_presets)})")
else:
print("✗ Presets list in zone not found")
except Exception as e:
print(f"✗ Drag and drop test error: {e}")
import traceback
traceback.print_exc()
except Exception as e:
print(f"✗ Browser test error: {e}")
import traceback
traceback.print_exc()
finally:
browser.cleanup_test_data()
browser.teardown()
print(f"\nBrowser preset drag and drop tests: {passed}/{total} passed")
return passed >= total - 2 # Allow up to 2 failures (drag test and adding presets can be flaky)
def main():
"""Run all browser tests."""
print("=" * 60)
print("LED Controller Browser Tests")
print(f"Testing against: {BASE_URL}")
print("=" * 60)
# On Pi OS Lite there is no browser by default; skip with exit 0 instead of failing
browser = BrowserTest(headless=True)
if not browser.setup():
print("\nSkipped (Pi OS Lite / no browser). Install chromium-browser and")
print("chromium-chromedriver to run browser tests, or run on Pi OS with desktop.")
sys.exit(0)
browser.teardown()
browser = BrowserTest(headless=False) # Set to True for headless mode
results = []
# Run browser tests
results.append(("Browser Connection", test_browser_connection(browser)))
results.append(("Tabs UI", test_tabs_ui(browser)))
results.append(("Profiles UI", test_profiles_ui(browser)))
results.append(("Presets UI", test_presets_ui(browser)))
results.append(("Color Palette UI", test_color_palette_ui(browser)))
results.append(("Preset Drag and Drop", test_preset_drag_and_drop(browser)))
# Summary
print("\n" + "=" * 60)
print("Browser Test Summary")
print("=" * 60)
all_passed = True
for name, passed in results:
status = "✓ PASS" if passed else "✗ FAIL"
print(f"{status} - {name}")
if not passed:
all_passed = False
print("=" * 60)
if all_passed:
print("✓ All browser tests passed!")
sys.exit(0)
else:
print("✗ Some browser tests failed")
sys.exit(1)
if __name__ == "__main__":
main()