diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/__init__.py | 1 | ||||
| -rw-r--r-- | tests/integration/README.md | 127 | ||||
| -rw-r--r-- | tests/integration/__init__.py | 1 | ||||
| -rw-r--r-- | tests/integration/conftest.py | 273 | ||||
| -rw-r--r-- | tests/integration/test_client_hub_messaging.py | 374 | ||||
| -rw-r--r-- | tests/integration/test_error_detection.py | 34 | ||||
| -rw-r--r-- | tests/integration/test_smoke.py | 71 | ||||
| -rw-r--r-- | tests/integration/test_ui.py | 136 |
8 files changed, 1017 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d4839a6 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests package diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..99f74a6 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,127 @@ +# Integration Tests with Selenium + +This directory contains Selenium-based integration tests for the web application. + +## Setup + +1. Install Python dependencies: +```bash +pip install -r requirements-test.txt +``` + +2. Chrome browser must be installed (ChromeDriver will be downloaded automatically) + +## Running Tests + +### Run all tests: +```bash +pytest +``` + +### Run specific test categories: +```bash +# Smoke tests only +pytest -m smoke + +# UI interaction tests +pytest -m ui + +# Network tests +pytest -m network +``` + +### Run in headless mode: +```bash +$env:HEADLESS="1"; pytest +``` + +### Run with verbose output: +```bash +pytest -v +``` + +### Run specific test file: +```bash +pytest tests/integration/test_smoke.py +``` + +### Run specific test function: +```bash +pytest tests/integration/test_smoke.py::test_page_loads +``` + +## How It Works + +The test suite automatically: +1. Starts a local HTTP server on a free port +2. Serves the application from the `ggj26/` directory +3. Runs tests against the local server +4. Shuts down the server when tests complete + +This ensures JavaScript features requiring a web server (like CORS, modules, etc.) work correctly. + +## Environment Variables + +- `APP_URL`: Override the automatic HTTP server to test against an external URL + ```bash + $env:APP_URL="http://localhost:8080"; pytest + ``` + +- `HEADLESS`: Set to "1" to run tests in headless mode (no visible browser) + ```bash + $env:HEADLESS="1"; pytest + ``` + +## Test Structure + +- `conftest.py`: Pytest fixtures and configuration + - `http_server`: Starts local HTTP server (session-scoped) + - `base_url`: URL for the application + - `driver`: Selenium WebDriver instance + - `app`: Loads the application + - `ready_app`: Loads app and waits for it to be ready + - `wait`: WebDriverWait helper + +- `test_smoke.py`: Basic smoke tests to verify core functionality +- `test_ui.py`: UI interaction tests (clicks, keyboard, mouse) + +## Writing New Tests + +Example test: +```python +import pytest +from selenium.webdriver.common.by import By + +@pytest.mark.smoke +def test_my_feature(app): + """Test description.""" + element = app.find_element(By.ID, "my-element") + assert element.is_displayed() +``` + +## Debugging + +### View browser during test execution: +Don't set `HEADLESS=1` - tests will run with visible browser. + +### Add breakpoints: +```python +import pdb; pdb.set_trace() +``` + +### Take screenshots on failure: +```python +def test_something(app): + try: + # test code + pass + except: + app.save_screenshot("failure.png") + raise +``` + +### Check console logs: +```python +logs = app.get_log("browser") +print(logs) +``` diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..a265048 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +# Integration tests package 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") diff --git a/tests/integration/test_client_hub_messaging.py b/tests/integration/test_client_hub_messaging.py new file mode 100644 index 0000000..f5765cf --- /dev/null +++ b/tests/integration/test_client_hub_messaging.py @@ -0,0 +1,374 @@ +""" +Integration tests for client-hub messaging using Selenium. +Tests that client doesn't receive its own Join message back. +""" +import pytest +import time +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC + +pytestmark = [pytest.mark.nondestructive] + + +@pytest.mark.integration +def test_client_does_not_receive_own_join_message(ready_app, wait): + """ + Test that when a client sends a Join message to the hub, + it doesn't receive it back through its own message handler. + + This test: + 1. Clicks "Host" to create a hub and connect host client + 2. Injects JS to monitor if the host client receives a Join message + 3. Verifies the host client does NOT receive its own Join message + """ + driver = ready_app + + # Inject monitoring script before clicking Host + driver.execute_script(""" + // Track if client receives Join message + window.clientReceivedOwnJoin = false; + + // Monkey-patch Client.handle_message to detect Join messages + (function() { + // Wait for modules to load + var checkInterval = setInterval(function() { + try { + var clientModule = require('client'); + var Client = clientModule.Client; + + if (Client && Client.prototype && Client.prototype.handle_message) { + var originalHandleMessage = Client.prototype.handle_message; + + Client.prototype.handle_message = function(callback_id, message_data) { + // Check if this is a Join message + if (message_data && Array.isArray(message_data) && message_data[0] === 'Join') { + console.log('[TEST] Client received Join message!'); + window.clientReceivedOwnJoin = true; + } + + // Call original method + return originalHandleMessage.call(this, callback_id, message_data); + }; + + console.log('[TEST] Successfully patched Client.handle_message'); + clearInterval(checkInterval); + } + } catch (e) { + // Modules not loaded yet, keep trying + } + }, 100); + + // Give up after 10 seconds + setTimeout(function() { + clearInterval(checkInterval); + }, 10000); + })(); + """) + + # Wait for Host button to be available + time.sleep(1) + + # Click Host button + try: + # Try to find and click Host button via JavaScript + clicked = driver.execute_script(""" + // Look for Host button in the UI + var buttons = document.querySelectorAll('canvas'); + if (buttons.length > 0) { + // Simulate click on canvas where Host button would be + var canvas = buttons[0]; + var event = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + clientX: canvas.width / 2, + clientY: canvas.height / 2 - 64 // Approximate position of Host button + }); + canvas.dispatchEvent(event); + return true; + } + return false; + """) + + if not clicked: + # Fallback: Try clicking the canvas directly + canvas = driver.find_element(By.ID, "canvas") + canvas.click() + except Exception as e: + pytest.fail(f"Failed to click Host button: {e}") + + # Wait for connection to establish + time.sleep(3) + + # Check if client received its own Join message + received_own_join = driver.execute_script("return window.clientReceivedOwnJoin || false;") + + # Get console logs for debugging + logs = driver.get_log("browser") + test_logs = [log['message'] for log in logs if '[TEST]' in log.get('message', '')] + + print(f"\n=== Test Debug Info ===") + print(f"Client received own Join: {received_own_join}") + print(f"Test logs: {test_logs}") + + # Assert client did NOT receive its own Join message + assert not received_own_join, "Client should not receive its own Join message back from hub" + + +@pytest.mark.integration +def test_hub_receives_join_message(ready_app, wait): + """ + Test that the hub correctly receives the Join message from a connecting client. + + This test: + 1. Clicks "Host" to create a hub + 2. Monitors hub.handle_message for Join messages + 3. Verifies the hub receives the Join message + """ + driver = ready_app + + # Inject monitoring script + driver.execute_script(""" + // Track if hub receives Join message + window.hubReceivedJoin = false; + window.joinMessageData = null; + + // Monkey-patch Hub.handle_message to detect Join messages + (function() { + var checkInterval = setInterval(function() { + try { + var hubModule = require('hub'); + var Hub = hubModule.Hub; + + if (Hub && Hub.prototype && Hub.prototype.handle_message) { + var originalHandleMessage = Hub.prototype.handle_message; + + Hub.prototype.handle_message = function(from_client, msgname, data) { + // Normalise arguments: depending on the Lua/JS bridge, + // the message array may arrive as `msgname` or `data`, + // and may be 0- or 1-indexed from JS. + var message = null; + if (Array.isArray(msgname)) { + message = msgname; + } else if (Array.isArray(data)) { + message = data; + } + var msg_type = null; + var msg_data = null; + if (message) { + msg_type = message[0] || message[1] || null; + msg_data = message[1] || message[2] || null; + } + if (msg_type === 'Join') { + console.log('[TEST] Hub received Join message from:', from_client); + window.hubReceivedJoin = true; + window.joinMessageData = msg_data; + } + + // Call original method + return originalHandleMessage.call(this, from_client, msgname, data); + }; + + console.log('[TEST] Successfully patched Hub.handle_message'); + clearInterval(checkInterval); + } + } catch (e) { + // Modules not loaded yet, keep trying + } + }, 100); + + setTimeout(function() { + clearInterval(checkInterval); + }, 10000); + })(); + """) + + # Wait for modules to load + time.sleep(1) + + # Click Host button (same approach as previous test) + try: + clicked = driver.execute_script(""" + var canvas = document.getElementById('canvas'); + if (canvas) { + var event = new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true, + clientX: canvas.width / 2, + clientY: canvas.height / 2 - 64 + }); + canvas.dispatchEvent(event); + return true; + } + return false; + """) + + if not clicked: + canvas = driver.find_element(By.ID, "canvas") + canvas.click() + except Exception as e: + pytest.fail(f"Failed to click Host button: {e}") + + # Wait for connection and message processing + time.sleep(3) + + # Debug world/hub state before assertions + world_debug = driver.execute_script(""" + try { + var world = typeof require === 'function' ? require('world') : null; + return { + hasWorld: !!world, + hasHub: !!(world && world.hub), + hasNetwork: !!(world && world.network) + }; + } catch (e) { + return { error: String(e) }; + } + """) + print(f"World debug: {world_debug}") + js_flags = driver.execute_script(""" + return { + clientConnected: !!window._clientConnectedToHub, + clientJoinPayload: window._clientJoinPayload || null, + hubJoinReceived: !!window._hubJoinReceived, + hubJoinData: window._hubJoinData || null + }; + """) + print(f"JS flags: {js_flags}") + + # Check if hub received Join message (Lua side exposes this via js_bridge) + hub_received_join = driver.execute_script("return (window.hubReceivedJoin || window._hubJoinReceived) || false;") + join_data = driver.execute_script("return window.joinMessageData || window._hubJoinData || null;") + + # Get console logs for debugging + logs = driver.get_log("browser") + test_logs = [log['message'] for log in logs if '[TEST]' in log.get('message', '')] + + print(f"\n=== Test Debug Info ===") + print(f"Hub received Join: {hub_received_join}") + print(f"Join data: {join_data}") + print(f"Test logs: {test_logs}") + + # Assert hub received the Join message + assert hub_received_join, "Hub should receive Join message from connecting client" + assert join_data is not None, "Join message should contain data" + # Check that join data has a 'name' field + if join_data: + assert 'name' in join_data, "Join message data should contain 'name' field" + + +@pytest.mark.integration +def test_message_flow_integrity(ready_app, wait): + """ + End-to-end test of message flow: client sends, hub receives, but client doesn't get echo. + + This combines both previous tests to verify the complete message flow. + """ + driver = ready_app + + # Inject comprehensive monitoring + driver.execute_script(""" + window.testResults = { + clientReceivedJoin: false, + hubReceivedJoin: false, + joinData: null + }; + + (function() { + var checkInterval = setInterval(function() { + try { + // Patch Client + var clientModule = require('client'); + var hubModule = require('hub'); + + if (clientModule && clientModule.Client && + hubModule && hubModule.Hub) { + + var Client = clientModule.Client; + var Hub = hubModule.Hub; + + // Patch client + if (Client.prototype.handle_message) { + var origClientHandle = Client.prototype.handle_message; + Client.prototype.handle_message = function(cid, mdata) { + if (mdata && mdata[0] === 'Join') { + console.log('[TEST] CLIENT RECEIVED JOIN - THIS IS THE BUG!'); + window.testResults.clientReceivedJoin = true; + } + return origClientHandle.call(this, cid, mdata); + }; + } + + // Patch hub + if (Hub.prototype.handle_message) { + var origHubHandle = Hub.prototype.handle_message; + Hub.prototype.handle_message = function(from, msgname, data) { + // Normalise message array from Lua/JS bridge. + var message = null; + if (Array.isArray(msgname)) { + message = msgname; + } else if (Array.isArray(data)) { + message = data; + } + var msg_type = null; + var msg_data = null; + if (message) { + msg_type = message[0] || message[1] || null; + msg_data = message[1] || message[2] || null; + } + if (msg_type === 'Join') { + console.log('[TEST] Hub received Join - CORRECT!'); + window.testResults.hubReceivedJoin = true; + window.testResults.joinData = msg_data; + } + return origHubHandle.call(this, from, msgname, data); + }; + } + + console.log('[TEST] Patching complete'); + clearInterval(checkInterval); + } + } catch (e) { + // Keep trying + } + }, 100); + + setTimeout(function() { clearInterval(checkInterval); }, 10000); + })(); + """) + + time.sleep(1) + + # Click Host + canvas = driver.find_element(By.ID, "canvas") + canvas.click() + + # Wait for messages to flow + time.sleep(3) + + # Get results, augmenting from Lua-side hub instrumentation if present + results = driver.execute_script(""" + if (typeof window.testResults === 'undefined') { + window.testResults = { + clientReceivedJoin: false, + hubReceivedJoin: false, + joinData: null + }; + } + if (window._hubJoinReceived) { + window.testResults.hubReceivedJoin = true; + } + if (window._hubJoinData) { + window.testResults.joinData = window._hubJoinData; + } + return window.testResults; + """) + + print(f"\n=== Complete Test Results ===") + print(f"Results: {results}") + + # Assertions + assert results['hubReceivedJoin'], "Hub must receive Join message" + assert not results['clientReceivedJoin'], "Client must NOT receive its own Join message (bug fixed!)" + assert results['joinData'] is not None, "Join message must have data" diff --git a/tests/integration/test_error_detection.py b/tests/integration/test_error_detection.py new file mode 100644 index 0000000..7cd9bf9 --- /dev/null +++ b/tests/integration/test_error_detection.py @@ -0,0 +1,34 @@ +""" +Test to verify that Amulet error detection is working. +This test checks if the error detection mechanism can detect errors. +""" +import pytest +from selenium.webdriver.common.by import By +import time + + +pytestmark = [pytest.mark.nondestructive] + + +@pytest.mark.smoke +def test_error_detection_mechanism(app): + """Test that the error detection mechanism is set up correctly.""" + # Wait for app to fully initialize + time.sleep(3) + + # Check if window.amuletErrors is defined + errors_defined = app.execute_script(""" + return typeof window.amuletErrors !== 'undefined'; + """) + + assert errors_defined, "window.amuletErrors is not defined - log_js_bridge may not be loaded" + + # Check initial state + error_count = app.execute_script("return window.amuletErrors.length;") + print(f"Initial error count: {error_count}") + + # If there are errors, print them + if error_count > 0: + errors = app.execute_script("return window.amuletErrors;") + for err in errors: + print(f" {err['level']}: {err['message']}") diff --git a/tests/integration/test_smoke.py b/tests/integration/test_smoke.py new file mode 100644 index 0000000..cc44165 --- /dev/null +++ b/tests/integration/test_smoke.py @@ -0,0 +1,71 @@ +""" +Smoke tests for basic application functionality. +""" +import pytest +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC + +pytestmark = [pytest.mark.nondestructive] + + +@pytest.mark.smoke +def test_page_loads(app): + """Test that the page loads successfully.""" + assert "Amulet" in app.title or app.title != "" + + +@pytest.mark.smoke +def test_canvas_exists(app): + """Test that the canvas element exists.""" + canvas = app.find_element(By.ID, "canvas") + assert canvas is not None + assert canvas.is_displayed() + + +@pytest.mark.smoke +def test_canvas_dimensions(app): + """Test that canvas has proper dimensions.""" + canvas = app.find_element(By.ID, "canvas") + + # Canvas should have width and height attributes + width = canvas.get_attribute("width") + height = canvas.get_attribute("height") + + assert width is not None + assert height is not None + assert int(width) > 0 + assert int(height) > 0 + + +@pytest.mark.smoke +def test_no_javascript_errors(app): + """Test that there are no JavaScript errors on page load.""" + # Get browser console logs + logs = app.get_log("browser") + + # Filter for severe errors (not warnings) + errors = [log for log in logs if log["level"] == "SEVERE"] + + # Filter out favicon 404 errors (expected) + errors = [log for log in errors if "favicon.ico" not in log.get("message", "")] + + # Assert no severe errors + assert len(errors) == 0, f"JavaScript errors found: {errors}" + + +@pytest.mark.smoke +def test_status_overlay_present(app): + """Test that the status overlay exists (for loading indication).""" + status_overlay = app.find_element(By.ID, "status-overlay") + assert status_overlay is not None + + +@pytest.mark.smoke +def test_container_structure(app): + """Test that the main container structure is correct.""" + container = app.find_element(By.ID, "container") + assert container is not None + + # Check for canvas inside container + canvas = container.find_element(By.ID, "canvas") + assert canvas is not None diff --git a/tests/integration/test_ui.py b/tests/integration/test_ui.py new file mode 100644 index 0000000..f86c74b --- /dev/null +++ b/tests/integration/test_ui.py @@ -0,0 +1,136 @@ +""" +UI interaction tests for the application. +""" +import pytest +from selenium.webdriver.common.by import By +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys +import time +import sys +import os + +# Import helper from conftest +sys.path.insert(0, os.path.dirname(__file__)) +from conftest import assert_no_javascript_errors + +pytestmark = [pytest.mark.nondestructive] + + +@pytest.mark.ui +def test_canvas_interaction(app): + """Test basic canvas interaction (click).""" + canvas = app.find_element(By.ID, "canvas") + + # Click on the canvas + ActionChains(app).move_to_element(canvas).click().perform() + + # Wait a moment for any click handlers + time.sleep(0.5) + + # Debug: Check what's in the error arrays + amulet_errors = app.execute_script("return window.amuletErrors;") + browser_logs = app.get_log("browser") + print(f"\n=== DEBUG INFO ===") + print(f"Amulet errors count: {len(amulet_errors) if amulet_errors else 0}") + if amulet_errors and len(amulet_errors) > 0: + print(f"Amulet errors: {amulet_errors}") + print(f"Browser log count: {len(browser_logs)}") + for log in browser_logs: + if log['level'] in ['SEVERE', 'WARNING']: + print(f" [{log['level']}] {log['message'][:200]}") + print(f"==================\n") + + # Check for JavaScript errors + assert_no_javascript_errors(app) + + # Verify canvas is still functional + assert canvas.is_displayed() + + +@pytest.mark.ui +def test_canvas_mouse_movement(app): + """Test mouse movement over canvas.""" + canvas = app.find_element(By.ID, "canvas") + + # Move mouse across canvas + actions = ActionChains(app) + actions.move_to_element_with_offset(canvas, 50, 50) + actions.move_by_offset(100, 100) + actions.perform() + + time.sleep(0.5) + assert_no_javascript_errors(app) + assert canvas.is_displayed() + + +@pytest.mark.ui +def test_keyboard_input(app): + """Test keyboard input to the application.""" + canvas = app.find_element(By.ID, "canvas") + + # Click canvas to focus + canvas.click() + time.sleep(0.3) + + # Send some keyboard input + actions = ActionChains(app) + actions.send_keys("w") + actions.send_keys("a") + actions.send_keys("s") + actions.send_keys("d") + actions.perform() + + time.sleep(0.5) + assert_no_javascript_errors(app) + assert canvas.is_displayed() + + +@pytest.mark.ui +def test_right_click_prevention(app): + """Test that right-click context menu is prevented on canvas.""" + canvas = app.find_element(By.ID, "canvas") + + # Attempt right-click (context menu should be prevented) + ActionChains(app).context_click(canvas).perform() + + time.sleep(0.3) + assert_no_javascript_errors(app) + + # Canvas should still be functional + assert canvas.is_displayed() + + +@pytest.mark.ui +def test_window_resize(app): + """Test that canvas handles window resize.""" + original_size = app.get_window_size() + + # Resize window + app.set_window_size(1280, 720) + time.sleep(0.5) + + canvas = app.find_element(By.ID, "canvas") + assert canvas.is_displayed() + assert_no_javascript_errors(app) + + # Restore original size + app.set_window_size(original_size["width"], original_size["height"]) + time.sleep(0.5) + + +@pytest.mark.ui +def test_canvas_drag(app): + """Test drag operations on canvas.""" + canvas = app.find_element(By.ID, "canvas") + + # Perform drag operation + actions = ActionChains(app) + actions.move_to_element_with_offset(canvas, 100, 100) + actions.click_and_hold() + actions.move_by_offset(50, 50) + actions.release() + actions.perform() + + time.sleep(0.5) + assert_no_javascript_errors(app) + assert canvas.is_displayed() |
