← back to Quadrille Showroom
scripts/test-senior.js
176 lines
#!/usr/bin/env node
/**
* test-senior.js — SENIOR-STYLE walkthrough. Drives ONLY the big on-screen guided
* buttons (Previous / Next / Auto Tour / All Designs) with the MOUSE. NO keyboard,
* NO 3D walking — proves a 65-year-old can browse the whole collection hands-on-mouse.
*
* Verifies: guided mode boots focused on a board; Next/Previous fly board-to-board;
* Auto Tour runs hands-free and Pause stops it; All Designs grid opens + a card jumps
* to a board; the big buttons are >=56px tall; the China Seas spec card stays intact;
* FPS stays healthy; zero console errors.
*
* Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/test-senior.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 = [], fpsLog = [];
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
// Ensure a clean GUIDED default (no persisted Explore from a prior run).
await page.addInitScript(() => { try { localStorage.removeItem('qh_explore'); } catch(e){} });
console.log('→ loading', URL);
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForFunction(() => window._wingBoards && window._wingBoards.length > 0, { timeout: 25000 }).catch(() => {});
await sleep(3500); // loader fade + guided auto-focus on the middle board
const sampleFps = async (tag) => {
const fps = await page.evaluate(() => { const el = document.getElementById('fps-counter'); return el ? parseInt(el.textContent) : null; });
if (fps > 0) fpsLog.push({ tag, fps });
return fps;
};
// --- Assert guided mode booted on a focused board (big title visible) ---
const guided = await page.evaluate(() => {
const t = document.getElementById('guided-title');
const bar = document.getElementById('guided-bar');
const exploreOff = !document.body.classList.contains('explore-mode');
return {
titleVisible: t && !t.classList.contains('hidden'),
barVisible: !!bar && getComputedStyle(bar).display !== 'none',
exploreOff,
titleText: document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : null,
pos: document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : null,
};
});
console.log(' guided boot:', JSON.stringify(guided));
await page.screenshot({ path: path.join(SHOTS, 'sr-01-guided-boot.png') });
await sampleFps('boot');
// --- Measure the big-button tap-target heights (must be >= 56px) ---
const btnSizes = await page.evaluate(() => {
const ids = ['g-prev', 'g-tour', 'g-next', 'g-grid'];
const out = {};
ids.forEach(id => { const el = document.getElementById(id); out[id] = el ? Math.round(el.getBoundingClientRect().height) : 0; });
return out;
});
console.log(' button heights:', JSON.stringify(btnSizes));
// --- Browse FORWARD with the big "Next ▶" button only (mouse clicks) ---
console.log('→ NEXT × 6 (mouse only, no keyboard)');
const seen = [];
for (let i = 0; i < 6; i++) {
await page.click('#g-next');
await sleep(1500);
const name = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
const pos = await page.evaluate(() => document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : '');
seen.push(pos);
if (i === 2) await page.screenshot({ path: path.join(SHOTS, 'sr-02-after-next.png') });
await sampleFps('next');
}
console.log(' positions seen via Next:', JSON.stringify(seen));
// --- Browse BACKWARD with "◀ Previous" ---
console.log('→ PREVIOUS × 3 (mouse only)');
for (let i = 0; i < 3; i++) { await page.click('#g-prev'); await sleep(1400); await sampleFps('prev'); }
await page.screenshot({ path: path.join(SHOTS, 'sr-03-after-prev.png') });
// --- AUTO TOUR (hands-free) then PAUSE — all via the one big brass button ---
console.log('→ AUTO TOUR (hands-free) for ~14s, then PAUSE');
await page.click('#g-tour'); // start
await sleep(2000);
const touring = await page.evaluate(() => document.getElementById('g-tour').classList.contains('playing'));
await sleep(12000);
await page.screenshot({ path: path.join(SHOTS, 'sr-04-auto-tour.png') });
await sampleFps('tour');
await page.click('#g-tour'); // pause (same button)
await sleep(1200);
const paused = await page.evaluate(() => !document.getElementById('g-tour').classList.contains('playing'));
console.log(' auto-tour started:', touring, '| paused via same button:', paused);
// --- ALL DESIGNS grid — open, then tap a big picture to jump to it ---
console.log('→ ALL DESIGNS grid: open + tap a card');
await page.click('#g-grid');
await sleep(1200);
const cardCount = await page.evaluate(() => document.querySelectorAll('#grid-items .go-card').length);
await page.screenshot({ path: path.join(SHOTS, 'sr-05-all-designs.png') });
console.log(' grid cards:', cardCount);
// Tap the 4th card
await page.click('#grid-items .go-card:nth-child(4)').catch(() => {});
await sleep(1800);
const gridClosed = await page.evaluate(() => !document.getElementById('grid-overlay').classList.contains('open'));
const afterCardName = await page.evaluate(() => document.getElementById('gt-name') ? document.getElementById('gt-name').textContent : '');
await page.screenshot({ path: path.join(SHOTS, 'sr-06-after-grid-pick.png') });
console.log(' grid closed after pick:', gridClosed, '| now showing:', afterCardName);
// --- Page across the WHOLE collection with Next, confirming the window advances ---
console.log('→ paging the full collection with Next (window should advance past 8)');
let maxPos = 0;
for (let i = 0; i < 12; i++) {
await page.click('#g-next');
await sleep(1100);
const idx = await page.evaluate(() => {
const el = document.getElementById('gt-pos'); if (!el) return 0;
const m = el.textContent.match(/Design\s+(\d+)/); return m ? parseInt(m[1]) : 0;
});
if (idx > maxPos) maxPos = idx;
}
console.log(' highest design index reached via Next:', maxPos);
await sampleFps('paging');
// --- 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);
});
console.log(' SPEC SHEET:', JSON.stringify(specText));
const stats = await page.evaluate(() => (window._getSceneStats ? window._getSceneStats() : null));
const fpsVals = fpsLog.map(f => f.fps).filter(n => n > 0);
const report = {
when: new Date().toISOString(),
mode: 'senior-buttons-only',
guidedBoot: guided,
buttonHeights: btnSizes,
buttonsMeetTarget: Object.values(btnSizes).every(h => h >= 56),
autoTourWorked: !!touring && !!paused,
gridCards: cardCount,
gridPickJumped: gridClosed,
highestIndexViaNext: maxPos,
specText,
errors, errorCount: errors.length,
fpsMin: fpsVals.length ? Math.min(...fpsVals) : null,
fpsAvg: fpsVals.length ? Math.round(fpsVals.reduce((a, b) => a + b, 0) / fpsVals.length) : null,
fpsSamples: fpsLog,
sceneStats: stats,
};
fs.writeFileSync(path.join(OUT, 'test-senior-report.json'), JSON.stringify(report, null, 2));
console.log('\n=== SENIOR REPORT ===');
console.log('errors:', report.errorCount, '| fps min/avg:', report.fpsMin, '/', report.fpsAvg,
'| buttons>=56:', report.buttonsMeetTarget, '| autoTour:', report.autoTourWorked,
'| gridPick:', report.gridPickJumped, '| reachedIndex:', report.highestIndexViaNext);
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-senior.webm')); console.log('✓ test-senior.webm'); }
})().catch(e => { console.error('SENIOR TEST FAILED:', e.message); process.exit(1); });