// @ts-check /** * Nexus Smoke Tests — Zero LLM, pure headless browser. * * Tests that the 3D world renders, modules load, and basic interaction works. * Run: npx playwright test tests/smoke.spec.js * Requires: a local server serving the nexus (e.g., python3 -m http.server 8888) */ const { test, expect } = require('@playwright/test'); const BASE_URL = process.env.NEXUS_URL || 'http://localhost:8888'; // --- RENDERING TESTS --- test.describe('World Renders', () => { test('index.html loads without errors', async ({ page }) => { const errors = []; page.on('pageerror', err => errors.push(err.message)); const response = await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); expect(response.status()).toBe(200); // Give Three.js a moment to initialize await page.waitForTimeout(2000); // No fatal JS errors const fatalErrors = errors.filter(e => !e.includes('ambient.mp3') && // missing audio file is OK !e.includes('favicon') && !e.includes('serviceWorker') ); expect(fatalErrors).toEqual([]); }); test('canvas element exists (Three.js rendered)', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Three.js creates a element const canvas = await page.locator('canvas'); await expect(canvas).toBeVisible(); // Canvas should have non-zero dimensions const box = await canvas.boundingBox(); expect(box.width).toBeGreaterThan(100); expect(box.height).toBeGreaterThan(100); }); test('canvas is not all black (scene has content)', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(4000); // Sample pixels from the canvas const hasContent = await page.evaluate(() => { const canvas = document.querySelector('canvas'); if (!canvas) return false; const ctx = canvas.getContext('webgl2') || canvas.getContext('webgl'); if (!ctx) return false; // Read a block of pixels from center const w = canvas.width; const h = canvas.height; const pixels = new Uint8Array(4 * 100); ctx.readPixels( Math.floor(w / 2) - 5, Math.floor(h / 2) - 5, 10, 10, ctx.RGBA, ctx.UNSIGNED_BYTE, pixels ); // Check if at least some pixels are non-black let nonBlack = 0; for (let i = 0; i < pixels.length; i += 4) { if (pixels[i] > 5 || pixels[i + 1] > 5 || pixels[i + 2] > 5) { nonBlack++; } } return nonBlack > 5; // At least 5 non-black pixels in the sample }); expect(hasContent).toBe(true); }); test('WebGL context is healthy', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2000); const glInfo = await page.evaluate(() => { const canvas = document.querySelector('canvas'); if (!canvas) return { error: 'no canvas' }; const gl = canvas.getContext('webgl2') || canvas.getContext('webgl'); if (!gl) return { error: 'no webgl context' }; return { renderer: gl.getParameter(gl.RENDERER), vendor: gl.getParameter(gl.VENDOR), isLost: gl.isContextLost(), }; }); expect(glInfo.error).toBeUndefined(); expect(glInfo.isLost).toBe(false); }); }); // --- MODULE LOADING TESTS --- test.describe('Modules Load', () => { test('all ES modules resolve (no import errors)', async ({ page }) => { const moduleErrors = []; page.on('pageerror', err => { if (err.message.includes('import') || err.message.includes('module') || err.message.includes('export') || err.message.includes('Cannot find')) { moduleErrors.push(err.message); } }); await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); expect(moduleErrors).toEqual([]); }); test('Three.js loaded from CDN', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2000); const hasThree = await page.evaluate(() => { // Check if THREE is accessible (it's imported as ES module, so check via scene) const canvas = document.querySelector('canvas'); return !!canvas; }); expect(hasThree).toBe(true); }); }); // --- HUD ELEMENTS --- test.describe('HUD Elements', () => { test('block height display exists', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); const blockDisplay = page.locator('#block-height-display'); await expect(blockDisplay).toBeAttached(); }); test('weather HUD exists', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); const weather = page.locator('#weather-hud'); await expect(weather).toBeAttached(); }); test('audio toggle exists', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); const btn = page.locator('#audio-toggle'); await expect(btn).toBeAttached(); }); test('sovereignty message exists', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); const msg = page.locator('#sovereignty-msg'); await expect(msg).toBeAttached(); }); test('oath overlay exists', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); const oath = page.locator('#oath-overlay'); await expect(oath).toBeAttached(); }); }); // --- INTERACTION TESTS --- test.describe('Interactions', () => { test('mouse movement updates camera (parallax)', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Get initial canvas snapshot const canvas = page.locator('canvas'); const box = await canvas.boundingBox(); // Take screenshot before mouse move const before = await page.screenshot({ clip: { x: box.x, y: box.y, width: 100, height: 100 } }); // Move mouse significantly await page.mouse.move(box.x + box.width * 0.8, box.y + box.height * 0.2); await page.waitForTimeout(1500); // Take screenshot after const after = await page.screenshot({ clip: { x: box.x, y: box.y, width: 100, height: 100 } }); // Screenshots should differ (camera moved) expect(Buffer.compare(before, after)).not.toBe(0); }); test('Tab key toggles overview mode', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); // Press Tab for overview await page.keyboard.press('Tab'); await page.waitForTimeout(1000); // Overview indicator should be visible const indicator = page.locator('#overview-indicator'); // It should have some visibility (either display or opacity) const isVisible = await indicator.evaluate(el => { const style = window.getComputedStyle(el); return style.display !== 'none' && parseFloat(style.opacity) > 0; }); // Press Tab again to exit await page.keyboard.press('Tab'); expect(isVisible).toBe(true); }); test('animation loop is running (requestAnimationFrame)', async ({ page }) => { await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2000); // Check that frames are being rendered by watching a timestamp const frameCount = await page.evaluate(() => { return new Promise(resolve => { let count = 0; const start = performance.now(); function tick() { count++; if (performance.now() - start > 500) { resolve(count); } else { requestAnimationFrame(tick); } } requestAnimationFrame(tick); }); }); // Should get at least 10 frames in 500ms (20+ FPS) expect(frameCount).toBeGreaterThan(10); }); }); // --- DATA / API TESTS --- test.describe('Data Loading', () => { test('portals.json loads', async ({ page }) => { const response = await page.goto(`${BASE_URL}/portals.json`); expect(response.status()).toBe(200); const data = await response.json(); expect(Array.isArray(data) || typeof data === 'object').toBe(true); }); test('sovereignty-status.json loads', async ({ page }) => { const response = await page.goto(`${BASE_URL}/sovereignty-status.json`); expect(response.status()).toBe(200); }); test('style.css loads', async ({ page }) => { const response = await page.goto(`${BASE_URL}/style.css`); expect(response.status()).toBe(200); }); test('manifest.json is valid', async ({ page }) => { const response = await page.goto(`${BASE_URL}/manifest.json`); expect(response.status()).toBe(200); const data = await response.json(); expect(data.name || data.short_name).toBeTruthy(); }); });