diff --git a/tests/smoke.spec.js b/tests/smoke.spec.js deleted file mode 100644 index cb521a2..0000000 --- a/tests/smoke.spec.js +++ /dev/null @@ -1,274 +0,0 @@ -// @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(); - }); -});