← back to Quadrille Showroom
scripts/test-integration.js
214 lines
#!/usr/bin/env node
/**
* test-integration.js — verifies the INTEGRATED 10-version experience:
* • the picker shows V1…V10
* • V1–V5 are the in-engine 3D versions (FPS ≥ 50 on V1, errorCount tracked)
* • V6–V10 open the REAL standalone mechanic FULL-SCREEN in an overlay iframe,
* each proto loads (right src, no errors inside the frame), the big senior
* "‹ Back to Showroom" button is present + returns cleanly to the picker
* • the numbered legend inside each mechanic feeds the Chosen tray (postMessage)
* • senior guided nav + 27×27 China Seas specs are intact
* • screenshots each V6–V10 in-app + a final 10-up contact sheet
*
* Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/test-integration.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));
const OVERLAY = {
V6: { route: '/proto/v6-colorriver.html', name: 'Color River' },
V7: { route: '/proto/v7-moodboard.html', name: 'Mood-Board Table' },
V8: { route: '/proto/v8-swipe.html', name: 'Swipe Tower' },
V9: { route: '/proto/v9-walkin.html', name: 'Walk-In Room' },
V10:{ route: '/proto/v10-concierge.html',name: 'Concierge Nook' },
};
(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 = [];
// capture console errors from BOTH the top page AND any iframe (proto)
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
page.on('frameattached', () => {});
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);
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 has V1…V10 ---
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) };
});
console.log(' rail:', JSON.stringify(rail));
// --- 2. FPS on the 3D versions V1-V5 ---
const threeD = {};
for (const key of ['V1','V2','V3','V4','V5']) {
await page.click(`#ver-rail .ver-chip[data-version="${key}"]`);
await sleep(3000);
await sampleFps(key + '-warm');
await sleep(900);
const fps = await sampleFps(key);
threeD[key] = { fps };
console.log(` ${key} (3D): fps=${fps}`);
}
// return to V1 baseline shot
await page.click('#ver-rail .ver-chip[data-version="V1"]'); await sleep(2500);
await page.screenshot({ path: path.join(SHOTS, 'ver-V1.png') });
// --- 3. V6–V10: open each mechanic full-screen, verify src + load + back ---
const overlay = {};
for (const key of ['V6','V7','V8','V9','V10']) {
const exp = OVERLAY[key];
await page.click(`#ver-rail .ver-chip[data-version="${key}"]`);
// wait for the overlay to open + the iframe to navigate to the right route
await page.waitForFunction(() => {
const h = document.getElementById('mechanic-overlay');
return h && h.classList.contains('open');
}, { timeout: 8000 }).catch(() => {});
// wait for the iframe to have the right src + its document to load
await sleep(500);
const frameSrc = await page.evaluate(() => {
const f = document.getElementById('mech-frame'); return f ? f.getAttribute('src') : null;
});
// grab the actual child frame object & wait for its body
let childOk = false, hasBack = false, legendCount = 0, frameUrl = null;
try {
const handle = await page.$('#mech-frame');
const frame = await handle.contentFrame();
if (frame) {
frameUrl = frame.url();
await frame.waitForSelector('#qh-proto-back', { timeout: 8000 }).catch(() => {});
hasBack = await frame.evaluate(() => !!document.getElementById('qh-proto-back')).catch(() => false);
legendCount = await frame.evaluate(() => document.querySelectorAll('#qh-proto-legend .qpl-row').length).catch(() => 0);
// proto-specific "did it actually render content" probe
childOk = await frame.evaluate(() => {
// any of these means the proto's own UI mounted
return !!(document.querySelector('canvas') ||
document.querySelector('.swatch, .tile, .card, .swatchTray, #app > *, .room, .app, .masthead'));
}).catch(() => false);
}
} catch (e) { /* frame access raced */ }
// let the proto finish its API pull + (for V6's hue-sort / V9's 3D room build) settle.
// wait until the proto's own loading indicator is gone, then a beat more.
try {
const frame = await (await page.$('#mech-frame')).contentFrame();
if (frame) {
await frame.waitForFunction(() => {
// proto boot/loading overlays: V6 #boot, V8/V9 .loading#loading; treat
// "no visible boot/loading overlay" as ready.
const overlays = ['#boot', '#loading', '.loading', '#loader', '.loader'];
return !overlays.some(sel => { const n = document.querySelector(sel);
return n && n.offsetParent !== null; });
}, { timeout: 9000 }).catch(() => {});
}
} catch (e) {}
await sleep(3500); // generous paint settle (V6 hue-sort, V9 3D room, fonts)
await page.screenshot({ path: path.join(SHOTS, `ver-${key}.png`) });
// --- click element #2 in the legend → should land in the Chosen tray ---
let chosenAfter = [];
try {
const frame = await (await page.$('#mech-frame')).contentFrame();
if (frame) {
const row2 = await frame.$('#qh-proto-legend .qpl-row[data-n="2"]');
if (row2) {
// open the legend first (it boots collapsed)
await frame.click('#qh-proto-legend .qpl-head').catch(() => {});
await sleep(250);
await row2.click().catch(() => {});
await sleep(400);
}
}
chosenAfter = await page.evaluate(() => window._versions.chosen.map(c => c.key));
} catch (e) {}
// --- Back button returns to the picker (a 3D scene) ---
let backWorks = false;
try {
const frame = await (await page.$('#mech-frame')).contentFrame();
if (frame) { await frame.click('#qh-proto-back'); }
await page.waitForFunction(() => {
const h = document.getElementById('mechanic-overlay');
return !h || !h.classList.contains('open');
}, { timeout: 5000 }).catch(() => {});
backWorks = await page.evaluate(() => {
const h = document.getElementById('mechanic-overlay');
const closed = !h || !h.classList.contains('open');
const railVisible = !!document.querySelector('#ver-rail');
return closed && railVisible;
});
} catch (e) {}
await sleep(1500);
overlay[key] = {
srcAttr: frameSrc, frameUrl, expectedRoute: exp.route,
srcMatches: !!frameSrc && frameSrc.indexOf(exp.route) >= 0,
childRendered: childOk, hasBackBtn: hasBack, legendRows: legendCount,
choseViaLegend: chosenAfter.indexOf(key + '.2') >= 0, backWorks
};
console.log(` ${key} (${exp.name}): src=${overlay[key].srcMatches} render=${childOk} back=${backWorks} legend=${legendCount} chose=${overlay[key].choseViaLegend}`);
}
// --- 4. senior guided nav intact (on a 3D version) ---
await page.click('#ver-rail .ver-chip[data-version="V1"]'); await sleep(2200);
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);
// --- 5. specs intact ---
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);
});
const fpsVals = fpsLog.map(f => f.fps).filter(n => n > 0);
const v1fps = (threeD.V1 || {}).fps || null;
const report = {
when: new Date().toISOString(),
rail, railHasAll10: rail.count === 10,
threeD, overlay,
overlayAllReal: Object.values(overlay).every(o => o.srcMatches && o.childRendered && o.hasBackBtn && o.backWorks),
seniorNavWorks: seniorWorks,
specText, specsIntact: !!(specText && specText.some(s => s.includes('27'))),
errors, errorCount: errors.length,
fpsMin: fpsVals.length ? Math.min(...fpsVals) : null,
fps3dMin: Math.min(...Object.values(threeD).map(v => v.fps).filter(n => n > 0)),
v1fps,
};
fs.writeFileSync(path.join(OUT, 'test-integration-report.json'), JSON.stringify(report, null, 2));
console.log('\n=== INTEGRATION REPORT ===');
console.log('rail 10:', report.railHasAll10, '| overlays all real:', report.overlayAllReal,
'| senior:', report.seniorNavWorks, '| specs:', report.specsIntact);
console.log('3D fps min:', report.fps3dMin, '| V1 fps:', report.v1fps, '| errors:', report.errorCount);
if (errors.length) console.log('ERRORS:', errors.slice(0, 12).join(' || '));
await ctx.close(); await browser.close();
})().catch(e => { console.error('INTEGRATION TEST FAILED:', e.stack || e.message); process.exit(1); });