""" Browser smoke tests for the Nexus 3D world. Uses Playwright to verify the DOM contract, Three.js initialization, portal loading, and loading screen flow. Refs: #686 """ import json import os import subprocess import time from pathlib import Path import pytest from playwright.sync_api import sync_playwright, expect REPO_ROOT = Path(__file__).resolve().parent.parent SCREENSHOT_DIR = REPO_ROOT / "test-screenshots" # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def http_server(): """Start a simple HTTP server for the Nexus static files.""" import http.server import threading port = int(os.environ.get("NEXUS_TEST_PORT", "9876")) handler = http.server.SimpleHTTPRequestHandler server = http.server.HTTPServer(("127.0.0.1", port), handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() time.sleep(0.3) yield f"http://127.0.0.1:{port}" server.shutdown() @pytest.fixture(scope="module") def browser_page(http_server): """Launch a headless browser and navigate to the Nexus.""" SCREENSHOT_DIR.mkdir(exist_ok=True) with sync_playwright() as pw: browser = pw.chromium.launch( headless=True, args=["--no-sandbox", "--disable-gpu"], ) context = browser.new_context( viewport={"width": 1280, "height": 720}, ignore_https_errors=True, ) page = context.new_page() # Collect console errors console_errors = [] page.on("console", lambda msg: console_errors.append(msg.text) if msg.type == "error" else None) page.goto(http_server, wait_until="domcontentloaded", timeout=30000) page._console_errors = console_errors yield page browser.close() # --------------------------------------------------------------------------- # Static asset tests # --------------------------------------------------------------------------- class TestStaticAssets: """Verify all contract files are serveable.""" REQUIRED_FILES = [ "index.html", "app.js", "style.css", "portals.json", "vision.json", "manifest.json", "gofai_worker.js", ] def test_index_html_served(self, http_server): """index.html must return 200.""" import urllib.request resp = urllib.request.urlopen(f"{http_server}/index.html") assert resp.status == 200 @pytest.mark.parametrize("filename", REQUIRED_FILES) def test_contract_file_served(self, http_server, filename): """Each contract file must return 200.""" import urllib.request try: resp = urllib.request.urlopen(f"{http_server}/{filename}") assert resp.status == 200 except Exception as e: pytest.fail(f"{filename} not serveable: {e}") # --------------------------------------------------------------------------- # DOM contract tests # --------------------------------------------------------------------------- class TestDOMContract: """Verify required DOM elements exist after page load.""" REQUIRED_ELEMENTS = { "nexus-canvas": "canvas", "hud": "div", "chat-panel": "div", "chat-input": "input", "chat-messages": "div", "chat-send": "button", "chat-toggle": "button", "debug-overlay": "div", "nav-mode-label": "span", "ws-status-dot": "span", "hud-location-text": "span", "portal-hint": "div", "spatial-search": "div", } @pytest.mark.parametrize("element_id,tag", list(REQUIRED_ELEMENTS.items())) def test_element_exists(self, browser_page, element_id, tag): """Element with given ID must exist in the DOM.""" el = browser_page.query_selector(f"#{element_id}") assert el is not None, f"#{element_id} ({tag}) missing from DOM" def test_canvas_has_webgl(self, browser_page): """The nexus-canvas must have a WebGL rendering context.""" has_webgl = browser_page.evaluate(""" () => { const c = document.getElementById('nexus-canvas'); if (!c) return false; const ctx = c.getContext('webgl2') || c.getContext('webgl'); return ctx !== null; } """) assert has_webgl, "nexus-canvas has no WebGL context" def test_title_contains_nexus(self, browser_page): """Page title should reference The Nexus.""" title = browser_page.title() assert "nexus" in title.lower() or "timmy" in title.lower(), f"Unexpected title: {title}" # --------------------------------------------------------------------------- # Loading flow tests # --------------------------------------------------------------------------- class TestLoadingFlow: """Verify the loading screen → enter prompt → HUD flow.""" def test_loading_screen_transitions(self, browser_page): """Loading screen should fade out and HUD should become visible.""" # Wait for loading to complete and enter prompt to appear try: browser_page.wait_for_selector("#enter-prompt", state="visible", timeout=15000) except Exception: # Enter prompt may have already appeared and been clicked pass # Try clicking the enter prompt if it exists enter = browser_page.query_selector("#enter-prompt") if enter and enter.is_visible(): enter.click() time.sleep(1) # HUD should now be visible hud = browser_page.query_selector("#hud") assert hud is not None, "HUD element missing" # After enter, HUD display should not be 'none' display = browser_page.evaluate("() => document.getElementById('hud').style.display") assert display != "none", "HUD should be visible after entering" # --------------------------------------------------------------------------- # Three.js initialization tests # --------------------------------------------------------------------------- class TestThreeJSInit: """Verify Three.js initialized properly.""" def test_three_loaded(self, browser_page): """THREE namespace should be available (via import map).""" # Three.js is loaded as ES module, check for canvas context instead has_canvas = browser_page.evaluate(""" () => { const c = document.getElementById('nexus-canvas'); return c && c.width > 0 && c.height > 0; } """) assert has_canvas, "Canvas not properly initialized" def test_canvas_dimensions(self, browser_page): """Canvas should fill the viewport.""" dims = browser_page.evaluate(""" () => { const c = document.getElementById('nexus-canvas'); return { width: c.width, height: c.height, ww: window.innerWidth, wh: window.innerHeight }; } """) assert dims["width"] > 0, "Canvas width is 0" assert dims["height"] > 0, "Canvas height is 0" # --------------------------------------------------------------------------- # Data contract tests # --------------------------------------------------------------------------- class TestDataContract: """Verify JSON data files are valid and well-formed.""" def test_portals_json_valid(self): """portals.json must parse as a non-empty JSON array.""" data = json.loads((REPO_ROOT / "portals.json").read_text()) assert isinstance(data, list), "portals.json must be an array" assert len(data) > 0, "portals.json must have at least one portal" def test_portals_have_required_fields(self): """Each portal must have id, name, status, destination.""" data = json.loads((REPO_ROOT / "portals.json").read_text()) required = {"id", "name", "status", "destination"} for i, portal in enumerate(data): missing = required - set(portal.keys()) assert not missing, f"Portal {i} missing fields: {missing}" def test_vision_json_valid(self): """vision.json must parse as valid JSON.""" data = json.loads((REPO_ROOT / "vision.json").read_text()) assert data is not None def test_manifest_json_valid(self): """manifest.json must have required PWA fields.""" data = json.loads((REPO_ROOT / "manifest.json").read_text()) for key in ["name", "start_url", "theme_color"]: assert key in data, f"manifest.json missing '{key}'" # --------------------------------------------------------------------------- # Screenshot / visual proof # --------------------------------------------------------------------------- class TestVisualProof: """Capture screenshots as visual validation evidence.""" def test_screenshot_initial_state(self, browser_page): """Take a screenshot of the initial page state.""" path = SCREENSHOT_DIR / "smoke-initial.png" browser_page.screenshot(path=str(path)) assert path.exists(), "Screenshot was not saved" assert path.stat().st_size > 1000, "Screenshot seems empty" def test_screenshot_after_enter(self, browser_page): """Take a screenshot after clicking through the enter prompt.""" enter = browser_page.query_selector("#enter-prompt") if enter and enter.is_visible(): enter.click() time.sleep(2) else: time.sleep(1) path = SCREENSHOT_DIR / "smoke-post-enter.png" browser_page.screenshot(path=str(path)) assert path.exists() def test_screenshot_fullscreen(self, browser_page): """Full-page screenshot for visual regression baseline.""" path = SCREENSHOT_DIR / "smoke-fullscreen.png" browser_page.screenshot(path=str(path), full_page=True) assert path.exists() # --------------------------------------------------------------------------- # Provenance in browser context # --------------------------------------------------------------------------- class TestBrowserProvenance: """Verify provenance from within the browser context.""" def test_page_served_from_correct_origin(self, http_server): """The page must be served from localhost, not a stale remote.""" import urllib.request resp = urllib.request.urlopen(f"{http_server}/index.html") content = resp.read().decode("utf-8", errors="replace") # Must not contain references to legacy matrix path assert "/Users/apayne/the-matrix" not in content, \ "index.html references legacy matrix path — provenance violation" def test_index_html_has_nexus_title(self, http_server): """index.html title must reference The Nexus.""" import urllib.request resp = urllib.request.urlopen(f"{http_server}/index.html") content = resp.read().decode("utf-8", errors="replace") assert "The Nexus" in content or "Timmy" in content, \ "index.html title does not reference The Nexus"