diff options
| author | Alex Pickering <alex@cogarr.net> | 2026-02-01 13:14:32 -0600 |
|---|---|---|
| committer | Alexander M Pickering <alex@cogarr.net> | 2026-02-01 13:14:32 -0600 |
| commit | 3a975db66a3711f34e8b64bb27a8eaca79fdeca9 (patch) | |
| tree | fcc12f8f9d638ff575c1963796de76b7628854b4 /tests/integration/conftest.py | |
| download | ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.tar.gz ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.tar.bz2 ggj26-3a975db66a3711f34e8b64bb27a8eaca79fdeca9.zip | |
Diffstat (limited to 'tests/integration/conftest.py')
| -rw-r--r-- | tests/integration/conftest.py | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..02302ca --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,273 @@ +""" +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") |
