aboutsummaryrefslogtreecommitdiff
path: root/tests/integration
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration')
-rw-r--r--tests/integration/README.md127
-rw-r--r--tests/integration/__init__.py1
-rw-r--r--tests/integration/conftest.py273
-rw-r--r--tests/integration/test_client_hub_messaging.py374
-rw-r--r--tests/integration/test_error_detection.py34
-rw-r--r--tests/integration/test_smoke.py71
-rw-r--r--tests/integration/test_ui.py136
7 files changed, 1016 insertions, 0 deletions
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()