← back to Quadrille Showroom
scripts/test-views.js
126 lines
#!/usr/bin/env node
/**
* test-views.js — DEBUG-OURSELVES harness for the View-Mode + Theme engine.
*
* Cycles through ALL 10 view modes and (in Hero) all 6 themes, captures a
* labelled screenshot of each into recordings/shots/ (view-*.png, theme-*.png),
* samples FPS + drawCalls + triangles per mode, and records one continuous
* .webm clip Steve can scrub to pick winners. Also runs guided Next within a
* board-presentation mode to prove the senior nav still works per-view.
*
* Output:
* recordings/shots/view-<n>-<key>.png (×10)
* recordings/shots/theme-<n>-<key>.png (×6)
* recordings/test-views.webm (scrub clip)
* recordings/test-views-report.json (per-mode FPS/draws/tris, errors)
*
* Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/test-views.js
*/
const path = require('path');
const fs = require('fs');
const { chromium } = require('playwright');
const URL = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
const OUT = path.join(__dirname, '..', 'recordings');
const SHOTS = path.join(OUT, 'shots');
fs.mkdirSync(SHOTS, { recursive: true });
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
(async () => {
const browser = await chromium.launch({ args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
recordVideo: { dir: OUT, size: { width: 1600, height: 900 } },
});
const page = await ctx.newPage();
const errors = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
// Clean slate — Hero boot, default theme.
await page.addInitScript(() => { try { localStorage.clear(); } catch (e) {} });
console.log('→ loading', URL);
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForFunction(() => window._viewmode && window._wingBoards && window._wingBoards.length > 0, { timeout: 25000 }).catch(() => {});
await sleep(4000); // loader fade + guided auto-focus
const sample = async () => page.evaluate(() => {
const s = window._getSceneStats();
const fpsEl = document.getElementById('fps-counter');
return { fps: fpsEl ? parseInt(fpsEl.textContent) : null, draws: s.drawCalls, tris: s.triangles, meshes: s.meshes };
});
const MODE_ORDER = await page.evaluate(() => window._viewmode.MODE_ORDER);
const MODE_LABELS = await page.evaluate(() => Object.fromEntries(window._viewmode.MODE_ORDER.map(k => [k, window._viewmode.MODES[k].label])));
const THEME_ORDER = await page.evaluate(() => window._viewmode.THEME_ORDER);
const THEME_LABELS = await page.evaluate(() => Object.fromEntries(window._viewmode.THEME_ORDER.map(k => [k, window._viewmode.THEMES[k].label])));
const report = { when: new Date().toISOString(), modes: [], themes: [], errors: [] };
// ---- VIEW MODES ----
console.log('→ cycling 10 view modes');
for (let i = 0; i < MODE_ORDER.length; i++) {
const key = MODE_ORDER[i];
await page.evaluate(k => window._viewmode.set(k), key);
// modes with motion (orbit/carousel/macro) get a longer settle so the shot is mid-motion
await sleep(['orbit', 'carousel', 'macro'].includes(key) ? 2600 : 1700);
const st = await sample();
const shot = path.join(SHOTS, `view-${i + 1}-${key}.png`);
await page.screenshot({ path: shot });
report.modes.push({ n: i + 1, key, label: MODE_LABELS[key], ...st, shot: path.basename(shot) });
console.log(` ${i + 1}. ${MODE_LABELS[key].padEnd(14)} fps=${st.fps} draws=${st.draws} tris=${st.tris}`);
}
// ---- GUIDED NEXT within a board-presentation mode (Macro) — prove senior nav works ----
console.log('→ guided Next within Macro (senior nav per-view)');
await page.evaluate(() => window._viewmode.set('macro'));
await sleep(1600);
const macroBefore = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
await page.click('#g-next'); await sleep(1800);
const macroAfter = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
report.guidedNextInMode = { before: macroBefore, after: macroAfter, advanced: macroBefore !== macroAfter };
console.log(` Macro Next: "${macroBefore}" → "${macroAfter}" advanced=${report.guidedNextInMode.advanced}`);
// ---- THEMES (shot in Hero so the swatch + room read cleanly) ----
console.log('→ cycling 6 themes (in Hero)');
await page.evaluate(() => window._viewmode.set('hero'));
await sleep(1500);
await page.evaluate(() => window._viewmode.perf(true)); // show perf readout in theme shots
for (let i = 0; i < THEME_ORDER.length; i++) {
const key = THEME_ORDER[i];
await page.evaluate(k => window._viewmode.theme(k), key);
await sleep(1500);
const st = await sample();
const shot = path.join(SHOTS, `theme-${i + 1}-${key}.png`);
await page.screenshot({ path: shot });
report.themes.push({ n: i + 1, key, label: THEME_LABELS[key], ...st, shot: path.basename(shot) });
console.log(` ${i + 1}. ${THEME_LABELS[key].padEnd(16)} fps=${st.fps} draws=${st.draws}`);
}
// ---- spec-card integrity (China Seas) ----
const specText = await page.evaluate(() => {
const grid = document.getElementById('detail-spec-grid');
if (!grid) return null;
return [...grid.querySelectorAll('.wd-spec')].map(r => r.querySelector('dt').textContent + ': ' + r.querySelector('dd').textContent);
});
report.specText = specText;
const fpsVals = [...report.modes, ...report.themes].map(m => m.fps).filter(n => n > 0);
report.fpsMin = fpsVals.length ? Math.min(...fpsVals) : null;
report.fpsAvg = fpsVals.length ? Math.round(fpsVals.reduce((a, b) => a + b, 0) / fpsVals.length) : null;
report.errors = errors; report.errorCount = errors.length;
fs.writeFileSync(path.join(OUT, 'test-views-report.json'), JSON.stringify(report, null, 2));
console.log('\n=== VIEW-MODE REPORT ===');
console.log('fps min/avg:', report.fpsMin, '/', report.fpsAvg, '| errors:', report.errorCount,
'| specs:', JSON.stringify(specText));
if (errors.length) console.log('ERRORS:', errors.slice(0, 8).join(' || '));
await page.close();
const vid = await page.video().path().catch(() => null);
await ctx.close(); await browser.close();
if (vid && fs.existsSync(vid)) { fs.renameSync(vid, path.join(OUT, 'test-views.webm')); console.log('✓ recordings/test-views.webm'); }
console.log('✓ shots in recordings/shots/ (view-*.png ×10, theme-*.png ×6)');
})().catch(e => { console.error('VIEW TEST FAILED:', e.message); process.exit(1); });