Files
the-nexus/tests/test_browser_smoke.py
Alexander Whitestone 2801b8a2ee
Some checks failed
CI / test (pull_request) Failing after 1m21s
Review Approval Gate / verify-review (pull_request) Successful in 12s
CI / validate (pull_request) Failing after 1m30s
fix: increase loading screen test wait for HUD visibility (#1510)
2026-04-15 02:52:58 +00:00

294 lines
11 KiB
Python

"""
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(2) # Wait for click handler + transition
# 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 "<title>The Nexus" in content or "Timmy" in content, \
"index.html title does not reference The Nexus"