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