#!/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()