""" Pytest configuration and fixtures for Selenium integration tests. """ import pytest import os import time import threading import socket from http.server import HTTPServer, SimpleHTTPRequestHandler from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from webdriver_manager.chrome import ChromeDriverManager def find_free_port(): """Find a free port to use for the HTTP server.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(('', 0)) s.listen(1) port = s.getsockname()[1] return port @pytest.fixture(scope="session") def http_server(): """Start a local HTTP server to serve the application.""" # Check if APP_URL is provided (to skip server for external testing) if os.getenv("APP_URL"): yield os.getenv("APP_URL") return # Find project root and ggj26 directory project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ggj26_dir = os.path.join(project_root, "ggj26") # Verify directory exists if not os.path.exists(ggj26_dir): raise FileNotFoundError(f"ggj26 directory not found at {ggj26_dir}") # Find a free port port = find_free_port() # Change to ggj26 directory for serving original_dir = os.getcwd() os.chdir(ggj26_dir) # Create HTTP server server = HTTPServer(('localhost', port), SimpleHTTPRequestHandler) # Start server in background thread server_thread = threading.Thread(target=server.serve_forever, daemon=True) server_thread.start() # Wait a moment for server to start time.sleep(0.5) base_url = f"http://localhost:{port}" print(f"\nStarted HTTP server at {base_url}") yield base_url # Cleanup server.shutdown() os.chdir(original_dir) @pytest.fixture(scope="session") def base_url(http_server): """Base URL for the application.""" return http_server @pytest.fixture(scope="function") def chrome_options(): """Chrome options for Selenium WebDriver.""" options = Options() # Set Chromium binary location chromium_path = os.getenv("CHROMIUM_PATH", r"D:\Programs\Chromium\chrome-win\chrome.exe") if os.path.exists(chromium_path): options.binary_location = chromium_path # Headless mode - set HEADLESS=1 to run tests without opening browser if os.getenv("HEADLESS") == "1": options.add_argument("--headless=new") # Additional options for stability options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") # Enable WebGL and canvas options.add_argument("--enable-webgl") options.add_argument("--ignore-gpu-blacklist") # Allow file access (needed for file:// protocol) options.add_argument("--allow-file-access-from-files") # Disable unnecessary features options.add_argument("--disable-extensions") options.add_experimental_option("excludeSwitches", ["enable-logging"]) return options @pytest.fixture(scope="session") def chrome_options_session(): """Chrome options for Selenium WebDriver (session-scoped).""" options = Options() # Set Chromium binary location chromium_path = os.getenv("CHROMIUM_PATH", r"D:\Programs\Chromium\chrome-win\chrome.exe") if os.path.exists(chromium_path): options.binary_location = chromium_path # Headless mode - set HEADLESS=1 to run tests without opening browser if os.getenv("HEADLESS") == "1": options.add_argument("--headless=new") # Additional options for stability options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") # Enable WebGL and canvas options.add_argument("--enable-webgl") options.add_argument("--ignore-gpu-blacklist") # Allow file access (needed for file:// protocol) options.add_argument("--allow-file-access-from-files") # Disable unnecessary features options.add_argument("--disable-extensions") options.add_experimental_option("excludeSwitches", ["enable-logging"]) return options @pytest.fixture(scope="session") def driver(chrome_options_session): """Selenium WebDriver instance (session-scoped - reused across all tests).""" # Get the Chrome/Chromium version to download matching ChromeDriver from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.core.os_manager import ChromeType # Install ChromeDriver that matches the Chromium version service = Service(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()) driver = webdriver.Chrome(service=service, options=chrome_options_session) driver.implicitly_wait(10) yield driver # Teardown - only happens once at end of session driver.quit() @pytest.fixture(scope="function") def wait(driver): """WebDriverWait instance with 10 second timeout.""" return WebDriverWait(driver, 10) @pytest.fixture(scope="function") def app(driver, base_url): """Load the application in the browser (reloads for each test).""" driver.get(base_url) # Wait for initial page load time.sleep(2) yield driver # Clear browser logs after each test to avoid cross-contamination try: driver.get_log("browser") except: pass def check_for_amulet_error(driver): """ Check if Amulet has logged any errors via the log_js_bridge. Returns tuple (has_error, error_text) """ try: # Check if window.amuletErrors exists and has any errors amulet_errors = driver.execute_script(""" if (typeof window.amuletErrors !== 'undefined' && window.amuletErrors.length > 0) { return window.amuletErrors; } return null; """) if amulet_errors and len(amulet_errors) > 0: # Take a screenshot for debugging import datetime timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") screenshot_path = f"error_screenshot_{timestamp}.png" driver.save_screenshot(screenshot_path) error_messages = [f"{err['level'].upper()}: {err['message']}" for err in amulet_errors] return True, f"Amulet errors detected:\n" + "\n".join(error_messages) + f"\n(screenshot: {screenshot_path})" return False, None except Exception as e: # If we can't check, assume no error return False, None def check_javascript_errors(driver, ignore_favicon=True): """ Check for JavaScript errors in the browser console. Returns a list of error messages (empty if no errors). """ logs = driver.get_log("browser") errors = [log for log in logs if log["level"] == "SEVERE"] # Filter out favicon 404 errors if requested if ignore_favicon: errors = [log for log in errors if "favicon.ico" not in log.get("message", "")] return errors def assert_no_javascript_errors(driver, ignore_favicon=True): """ Assert that there are no JavaScript errors in the browser console. Also checks for Amulet error screens. Raises AssertionError if errors are found. """ # Check for Amulet blue screen errors first from selenium.webdriver.common.by import By has_amulet_error, error_msg = check_for_amulet_error(driver) if has_amulet_error: pytest.fail(f"Amulet error detected: {error_msg}") # Then check console logs errors = check_javascript_errors(driver, ignore_favicon) assert len(errors) == 0, f"JavaScript errors found: {errors}" def wait_for_canvas_ready(driver, timeout=30): """ Wait for the Amulet canvas to be ready. Returns True when canvas is visible and status overlay is hidden. """ wait = WebDriverWait(driver, timeout) # Wait for status overlay to disappear (indicates loading is complete) from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC try: # Wait for status-overlay to have display:none or be removed wait.until( EC.invisibility_of_element_located((By.ID, "status-overlay")) ) return True except: return False @pytest.fixture(scope="function") def ready_app(app): """Load app and wait for it to be ready.""" if wait_for_canvas_ready(app): return app else: pytest.fail("Application did not load within timeout period")