diff --git a/test.js b/test.js new file mode 100644 index 0000000..9e4999a --- /dev/null +++ b/test.js @@ -0,0 +1,150 @@ +#!/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', //i], + [' attribute', /]+lang=/i], + ['charset meta tag', /]+charset/i], + ['viewport meta tag', /]+viewport/i], + [' 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'); +}