aboutsummaryrefslogtreecommitdiff
path: root/tests/integration/conftest.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration/conftest.py')
-rw-r--r--tests/integration/conftest.py273
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")