delete: tests/smoke.spec.js — does not serve heartbeat/harness/portal (#548)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
This commit is contained in:
@@ -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 <canvas> 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user