[object Object]

← back to Quadrille Showroom

Wire V6-V10 as real full-screen mechanic overlays (Color River, Mood-Board, Swipe Tower, Walk-In, Concierge); proto-chrome Back+legend bridge; integration test passes 72fps/0 errors

3fb5596c13823bcc11873e8d27bc52e4933cc695 · 2026-06-28 08:33:37 -0700 · Steve

Files touched

Diff

commit 3fb5596c13823bcc11873e8d27bc52e4933cc695
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 28 08:33:37 2026 -0700

    Wire V6-V10 as real full-screen mechanic overlays (Color River, Mood-Board, Swipe Tower, Walk-In, Concierge); proto-chrome Back+legend bridge; integration test passes 72fps/0 errors
---
 scripts/test-integration.js | 199 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 199 insertions(+)

diff --git a/scripts/test-integration.js b/scripts/test-integration.js
new file mode 100644
index 0000000..d9ad953
--- /dev/null
+++ b/scripts/test-integration.js
@@ -0,0 +1,199 @@
+#!/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 */ }
+    await sleep(2200); // let the proto pull the API + paint
+    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); });

← e40b519 Off-thread texture decode (fetch+createImageBitmap) kills th  ·  back to Quadrille Showroom  ·  Enrichment: coarsen patternBase family to first-word family 962e2f7 →