Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Co-authored-by: Claude (Opus 4.6) <claude@hermes.local> Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
151 lines
5.4 KiB
JavaScript
151 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Nexus Test Harness
|
|
* Validates the scene loads without errors using only Node.js built-ins.
|
|
* Run: node test.js
|
|
*/
|
|
|
|
import { execSync } from 'child_process';
|
|
import { readFileSync, statSync } from 'fs';
|
|
import { resolve, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function pass(name) {
|
|
console.log(` ✓ ${name}`);
|
|
passed++;
|
|
}
|
|
|
|
function fail(name, reason) {
|
|
console.log(` ✗ ${name}`);
|
|
if (reason) console.log(` → ${reason}`);
|
|
failed++;
|
|
}
|
|
|
|
function section(name) {
|
|
console.log(`\n${name}`);
|
|
}
|
|
|
|
// ── Syntax checks ──────────────────────────────────────────────────────────
|
|
section('JS Syntax');
|
|
|
|
for (const file of ['app.js', 'ws-client.js']) {
|
|
try {
|
|
execSync(`node --check ${resolve(__dirname, file)}`, { stdio: 'pipe' });
|
|
pass(`${file} parses without syntax errors`);
|
|
} catch (e) {
|
|
fail(`${file} syntax check`, e.stderr?.toString().trim() || e.message);
|
|
}
|
|
}
|
|
|
|
// ── File size budget ────────────────────────────────────────────────────────
|
|
section('File Size Budget (< 500 KB)');
|
|
|
|
for (const file of ['app.js', 'ws-client.js']) {
|
|
try {
|
|
const bytes = statSync(resolve(__dirname, file)).size;
|
|
const kb = (bytes / 1024).toFixed(1);
|
|
if (bytes < 500 * 1024) {
|
|
pass(`${file} is ${kb} KB`);
|
|
} else {
|
|
fail(`${file} exceeds 500 KB budget`, `${kb} KB`);
|
|
}
|
|
} catch (e) {
|
|
fail(`${file} size check`, e.message);
|
|
}
|
|
}
|
|
|
|
// ── JSON validation ─────────────────────────────────────────────────────────
|
|
section('JSON Files');
|
|
|
|
for (const file of ['manifest.json', 'portals.json', 'vision.json']) {
|
|
try {
|
|
const raw = readFileSync(resolve(__dirname, file), 'utf8');
|
|
JSON.parse(raw);
|
|
pass(`${file} is valid JSON`);
|
|
} catch (e) {
|
|
fail(`${file}`, e.message);
|
|
}
|
|
}
|
|
|
|
// ── HTML structure ──────────────────────────────────────────────────────────
|
|
section('HTML Structure (index.html)');
|
|
|
|
const html = (() => {
|
|
try { return readFileSync(resolve(__dirname, 'index.html'), 'utf8'); }
|
|
catch (e) { fail('index.html readable', e.message); return ''; }
|
|
})();
|
|
|
|
if (html) {
|
|
const checks = [
|
|
['DOCTYPE declaration', /<!DOCTYPE html>/i],
|
|
['<html lang> attribute', /<html[^>]+lang=/i],
|
|
['charset meta tag', /<meta[^>]+charset/i],
|
|
['viewport meta tag', /<meta[^>]+viewport/i],
|
|
['<title> tag', /<title>[^<]+<\/title>/i],
|
|
['importmap script', /<script[^>]+type="importmap"/i],
|
|
['three.js in importmap', /"three"\s*:/],
|
|
['app.js module script', /<script[^>]+type="module"[^>]+src="app\.js"/i],
|
|
['debug-toggle element', /id="debug-toggle"/],
|
|
['</html> closing tag', /<\/html>/i],
|
|
];
|
|
|
|
for (const [name, re] of checks) {
|
|
if (re.test(html)) {
|
|
pass(name);
|
|
} else {
|
|
fail(name, `pattern not found: ${re}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── app.js static analysis ──────────────────────────────────────────────────
|
|
section('app.js Scene Components');
|
|
|
|
const appJs = (() => {
|
|
try { return readFileSync(resolve(__dirname, 'app.js'), 'utf8'); }
|
|
catch (e) { fail('app.js readable', e.message); return ''; }
|
|
})();
|
|
|
|
if (appJs) {
|
|
const checks = [
|
|
['NEXUS.colors palette defined', /const NEXUS\s*=\s*\{/],
|
|
['THREE.Scene created', /new THREE\.Scene\(\)/],
|
|
['THREE.PerspectiveCamera created', /new THREE\.PerspectiveCamera\(/],
|
|
['THREE.WebGLRenderer created', /new THREE\.WebGLRenderer\(/],
|
|
['renderer appended to DOM', /document\.body\.appendChild\(renderer\.domElement\)/],
|
|
['animate function defined', /function animate\s*\(\)/],
|
|
['requestAnimationFrame called', /requestAnimationFrame\(animate\)/],
|
|
['renderer.render called', /renderer\.render\(scene,\s*camera\)/],
|
|
['resize handler registered', /addEventListener\(['"]resize['"]/],
|
|
['clock defined', /new THREE\.Clock\(\)/],
|
|
['star field created', /new THREE\.Points\(/],
|
|
['constellation lines built', /buildConstellationLines/],
|
|
['ws-client imported', /import.*ws-client/],
|
|
['wsClient.connect called', /wsClient\.connect\(\)/],
|
|
];
|
|
|
|
for (const [name, re] of checks) {
|
|
if (re.test(appJs)) {
|
|
pass(name);
|
|
} else {
|
|
fail(name, `pattern not found: ${re}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
console.log(`\n${'─'.repeat(50)}`);
|
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\nSome tests failed. Fix the issues above before committing.\n');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('\nAll tests passed.\n');
|
|
}
|