← 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); });