← back to Quadrille Showroom
scripts/test-versions.js
180 lines
#!/usr/bin/env node
/**
* test-versions.js — verifies the "10 VERSIONS" system end-to-end with Playwright.
*
* Asserts: the version rail renders V1…V10 with clearly-visible numbers; switching
* versions rebuilds cleanly (no console errors, FPS stays healthy); the numbered
* overlay toggles ON and pins appear; clicking a pin adds it to the Chosen tray;
* the senior guided nav + collapsed popup are untouched; the China Seas specs are
* intact. Captures recordings/shots/ver-*.png for each version + the overlay.
*
* Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/test-versions.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 } });
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));
// Clean slate: no persisted version/overlay/chosen from a prior run.
await page.addInitScript(() => {
try { ['qh_explore','qh_version','qh_overlay','qh_chosen','qh_tray_open','qh_ver_rail'].forEach(k => localStorage.removeItem(k)); } 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 page.waitForFunction(() => window._versions && window._versions.VERSION_ORDER, { timeout: 15000 }).catch(() => {});
await sleep(3800); // loader fade + viewmodes boot + versions boot (V1)
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;
};
// --- 1. Rail renders V1…V10 with visible numbers ---
const rail = await page.evaluate(() => {
const chips = [...document.querySelectorAll('#ver-rail .ver-chip')];
return {
count: chips.length,
nums: chips.map(c => (c.querySelector('.vc-num') || {}).textContent),
names: chips.map(c => (c.querySelector('.vc-name') || {}).textContent),
activeBoot: (document.querySelector('#ver-rail .ver-chip.active') || {}).dataset
? document.querySelector('#ver-rail .ver-chip.active').dataset.version : null
};
});
console.log(' rail:', JSON.stringify(rail));
await page.screenshot({ path: path.join(SHOTS, 'ver-rail.png') });
// --- 2. Boot is V1, matches reference framing ---
await page.screenshot({ path: path.join(SHOTS, 'ver-V1.png') });
await sampleFps('V1');
// --- 3. Cycle every version, screenshot each, watch FPS + errors ---
const order = await page.evaluate(() => window._versions.VERSION_ORDER);
const perVersion = {};
for (const key of order) {
await page.click(`#ver-rail .ver-chip[data-version="${key}"]`);
await sleep(3200); // let the camera fly + the look fully settle before sampling steady-state FPS
await sampleFps(key + '-warm'); // discard the first read (may catch a settling frame)
await sleep(900);
const fps = await sampleFps(key);
const cur = await page.evaluate(() => window._versions.current);
perVersion[key] = { fps, applied: cur === key };
await page.screenshot({ path: path.join(SHOTS, `ver-${key}.png`) });
console.log(` ${key}: fps=${fps} applied=${cur === key}`);
}
// --- 4. Back to V1, turn ON the numbered overlay ---
await page.click(`#ver-rail .ver-chip[data-version="V1"]`);
await sleep(2200);
await page.click('#vr-overlay-btn');
await sleep(900);
const pins = await page.evaluate(() => {
const ps = [...document.querySelectorAll('#pin-layer .el-pin')];
const visible = ps.filter(p => p.style.display !== 'none');
return { total: ps.length, visible: visible.length, overlayOn: window._versions.overlayOn,
nums: visible.map(p => (p.querySelector('.pin-badge') || {}).textContent) };
});
console.log(' overlay pins:', JSON.stringify(pins));
await page.screenshot({ path: path.join(SHOTS, 'ver-overlay-V1.png') });
// --- 5. Click two pins → they land in the Chosen tray ---
// click the first two visible pins via force (pins are absolutely-positioned over canvas)
const pinEls = await page.$$('#pin-layer .el-pin');
let clicked = 0;
for (const pe of pinEls) {
const disp = await pe.evaluate(n => n.style.display);
if (disp !== 'none' && clicked < 2) { await pe.click({ force: true }).catch(() => {}); clicked++; await sleep(500); }
}
await sleep(400);
// open the tray so the chips render (force — it sits bottom-right, may be partly clipped at this viewport)
await page.click('#chosen-tray-head', { force: true }).catch(() => {});
await sleep(500);
const tray = await page.evaluate(() => ({
count: document.getElementById('ct-count') ? document.getElementById('ct-count').textContent : null,
chips: [...document.querySelectorAll('#chosen-items .ct-chip')].map(c =>
(c.querySelector('.ctc-tag') || {}).textContent + ' ' + (c.querySelector('.ctc-name') || {}).textContent),
chosenLib: window._versions.chosen.map(c => c.key),
chosenText: window._versions.chosenText()
}));
console.log(' tray:', JSON.stringify(tray));
await page.screenshot({ path: path.join(SHOTS, 'ver-tray.png') });
// --- 6. Re-visited chosen pins show the checked state ---
const checkState = await page.evaluate(() =>
[...document.querySelectorAll('#pin-layer .el-pin.chosen')].map(p => p.dataset.key));
console.log(' chosen pins (check state):', JSON.stringify(checkState));
// --- 7. LOCKED: senior guided nav still works under the version system ---
// close the tray + overlay so neither intercepts the bottom-bar clicks
await page.evaluate(() => { const t = document.getElementById('chosen-tray'); if (t) t.classList.remove('open'); });
await page.click('#vr-overlay-btn', { force: true }).catch(() => {}); // overlay off
await sleep(500);
const beforePos = await page.evaluate(() => document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : '');
await page.click('#g-next'); await sleep(1500);
const afterPos = await page.evaluate(() => document.getElementById('gt-pos') ? document.getElementById('gt-pos').textContent : '');
const seniorWorks = beforePos !== afterPos || (afterPos && afterPos.length > 0);
console.log(' senior Next:', beforePos, '→', afterPos, '| works:', seniorWorks);
// --- 8. Specs intact (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(' SPECS:', JSON.stringify(specText));
// --- 9. Collapsed popup default ---
const popupCollapsed = await page.evaluate(() => {
const wd = document.getElementById('wing-detail');
return wd ? wd.classList.contains('collapsed') : null;
});
const fpsVals = fpsLog.map(f => f.fps).filter(n => n > 0);
const report = {
when: new Date().toISOString(),
rail,
railHasAll10: rail.count === 10,
numbersVisible: rail.nums.every(n => /^V\d+$/.test((n || '').trim())),
perVersion,
allVersionsApply: Object.values(perVersion).every(v => v.applied),
overlay: pins,
overlayWorks: pins.overlayOn && pins.visible > 0,
tray,
trayWorks: parseInt(tray.count) >= 1 && tray.chips.length >= 1 && tray.chosenLib.length >= 1,
checkStatePins: checkState,
seniorNavWorks: seniorWorks,
specText,
specsIntact: !!(specText && specText.some(s => s.includes('27'))),
popupCollapsed,
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
};
fs.writeFileSync(path.join(OUT, 'test-versions-report.json'), JSON.stringify(report, null, 2));
console.log('\n=== VERSIONS REPORT ===');
console.log('rail 10:', report.railHasAll10, '| numbers visible:', report.numbersVisible,
'| all apply:', report.allVersionsApply, '| overlay:', report.overlayWorks,
'| tray:', report.trayWorks, '| senior:', report.seniorNavWorks,
'| specs:', report.specsIntact, '| popupCollapsed:', report.popupCollapsed);
console.log('errors:', report.errorCount, '| fps min/avg:', report.fpsMin, '/', report.fpsAvg);
if (errors.length) console.log('ERRORS:', errors.slice(0, 10).join(' || '));
await ctx.close(); await browser.close();
})().catch(e => { console.error('VERSIONS TEST FAILED:', e.message); process.exit(1); });