← back to Quadrille Showroom

cta/targeted-test.js

482 lines

'use strict';
const {chromium, webkit} = require('playwright');
const fs = require('fs');
const path = require('path');

const SHOTS = '/Users/macstudio3/Projects/quadrille-showroom/cta/shots';

async function testEngine(eng, launcher) {
  console.log('\n=== ENGINE:', eng, '===');
  const browser = await launcher.launch({
    headless: true,
    args: eng === 'chrome'
      ? ['--no-sandbox','--disable-setuid-sandbox','--enable-webgl','--use-gl=swiftshader','--ignore-gpu-blocklist']
      : []
  });
  const ctx = await browser.newContext({ viewport: {width:1600, height:900} });
  const page = await ctx.newPage();
  const errs = [];
  page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); });
  page.on('pageerror', e => errs.push('PAGEERR:' + e));

  fs.mkdirSync(path.join(SHOTS, eng), { recursive: true });

  const results = [];
  const R = (comp, ok, note) => {
    results.push({ eng, comp, ok, note: note || '' });
    console.log(' ', ok ? 'PASS' : 'FAIL', comp, note ? '| ' + note : '');
  };

  await page.goto('http://127.0.0.1:7690/', { waitUntil: 'domcontentloaded' });

  // Wait for ver-rail
  try {
    await page.waitForSelector('#ver-rail', { timeout: 20000 });
  } catch (e) {
    console.log('ver-rail timeout');
  }
  await page.waitForTimeout(2000);

  await page.screenshot({ path: path.join(SHOTS, eng, 'initial-state.png') });

  // ── 1. VERSION RAIL ───────────────────────────────────────────────────────
  const verState = await page.evaluate(() => {
    const rail = document.getElementById('ver-rail');
    const chips = document.querySelectorAll('.ver-chip');
    return {
      railExists: !!rail,
      railVisible: rail ? (rail.offsetWidth > 0) : false,
      chipCount: chips.length,
      firstChip: chips.length > 0 ? { text: chips[0].textContent.trim().substring(0,30), classes: chips[0].className } : null,
      currentVersion: window._versions ? window._versions.current : null,
    };
  });
  console.log('verState:', JSON.stringify(verState));
  R('Version rail (#ver-rail) exists', verState.railExists, 'chips=' + verState.chipCount + ' current=' + verState.currentVersion);
  R('Version rail has 11 chips', verState.chipCount >= 11, 'count=' + verState.chipCount);

  // ── 2. CLICK EACH VER-CHIP ────────────────────────────────────────────────
  const chipCount = await page.evaluate(() => document.querySelectorAll('.ver-chip').length);
  for (let i = 0; i < chipCount; i++) {
    const chipInfo = await page.evaluate((idx) => {
      const chip = document.querySelectorAll('.ver-chip')[idx];
      if (!chip) return null;
      return { text: chip.textContent.trim().substring(0, 30), classes: chip.className };
    }, i);
    if (!chipInfo) continue;

    // Click via evaluate (avoids pointer interception)
    await page.evaluate((idx) => {
      const chip = document.querySelectorAll('.ver-chip')[idx];
      if (chip) chip.click();
    }, i);
    await page.waitForTimeout(1800);

    const afterClick = await page.evaluate((idx) => {
      const chip = document.querySelectorAll('.ver-chip')[idx];
      const overlayHost = document.getElementById('mechanic-overlay');
      return {
        isActive: chip ? chip.classList.contains('active') : false,
        currentVersion: window._versions ? window._versions.current : null,
        overlayVisible: overlayHost ? (overlayHost.offsetWidth > 0 && overlayHost.style.display !== 'none') : false,
        canvasOk: (document.getElementById('showroom-canvas') || {}).offsetWidth > 0,
      };
    }, i);

    // For overlay versions (i >= 5), check overlay appeared
    if (i >= 5) {
      R('Version chip ' + i + ' (' + chipInfo.text + ') overlay', afterClick.overlayVisible || afterClick.isActive,
        'active=' + afterClick.isActive + ' overlay=' + afterClick.overlayVisible + ' ver=' + afterClick.currentVersion);
      // Close overlay
      await page.evaluate(() => {
        const h = document.getElementById('mechanic-overlay');
        if (h) h.style.display = 'none';
        document.body.classList.remove('mechanic-open');
        if (window._versions && window._versions.set) window._versions.set('V1');
      });
      await page.waitForTimeout(500);
    } else {
      R('Version chip ' + i + ' (' + chipInfo.text + ') 3D', afterClick.isActive,
        'ver=' + afterClick.currentVersion + ' canvas=' + afterClick.canvasOk);
    }
  }

  // Return to V1
  await page.evaluate(() => {
    if (window._versions && window._versions.set) window._versions.set('V1');
  });
  await page.waitForTimeout(1500);

  // ── 3. NUMBERS OVERLAY ───────────────────────────────────────────────────
  await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-before.png') });
  await page.evaluate(() => {
    const btn = document.getElementById('vr-overlay-btn');
    if (btn) btn.click();
  });
  await page.waitForTimeout(1200);

  const overlayState = await page.evaluate(() => {
    const pins = document.querySelectorAll('.el-pin');
    const pincodes = document.querySelectorAll('.pin-code');
    const firstCode = pincodes.length > 0 ? pincodes[0].textContent.trim() : '';
    const lastCode = pincodes.length > 0 ? pincodes[pincodes.length - 1].textContent.trim() : '';
    return { pinCount: pins.length, pincodeCount: pincodes.length, firstCode, lastCode };
  });
  console.log('Numbers overlay:', JSON.stringify(overlayState));
  R('Numbers overlay: pins appear', overlayState.pinCount > 0, 'pins=' + overlayState.pinCount);
  R('Numbers overlay: pin-code chips present', overlayState.pincodeCount > 0, 'codes=' + overlayState.pincodeCount + ' first=' + overlayState.firstCode);
  R('Numbers overlay: codes start with 1000QHV1', overlayState.firstCode.startsWith('1000QHV'), 'firstCode=' + overlayState.firstCode);
  await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-on.png') });

  // ── 4. PIN CLICK → CHOSEN TRAY ───────────────────────────────────────────
  // Note: the count element is #ct-count (in chosen-tray), not #tray-count (old panel)
  const pinClickResult = await page.evaluate(() => {
    const pin = document.querySelector('.el-pin');
    if (!pin) return { pinFound: false };
    pin.click();
    // ct-count is inside the chosen tray
    const ctCount = document.getElementById('ct-count');
    const chosenLen = window._versions ? window._versions.chosen.length : -1;
    return {
      pinFound: true,
      ctCount: ctCount ? parseInt(ctCount.textContent) || 0 : -1,
      chosenLen,
    };
  });
  await page.waitForTimeout(600);
  const trayCountAfter = await page.evaluate(() => {
    const ctCount = document.getElementById('ct-count');
    const chosenLen = window._versions ? window._versions.chosen.length : -1;
    return { ctCount: ctCount ? parseInt(ctCount.textContent) || 0 : -1, chosenLen };
  });
  console.log('Pin click:', JSON.stringify(pinClickResult), 'tray after:', JSON.stringify(trayCountAfter));
  R('Pin click → Chosen tray increments (ct-count / _versions.chosen)', trayCountAfter.chosenLen > 0, 'chosenLen=' + trayCountAfter.chosenLen + ' ctCount=' + trayCountAfter.ctCount);
  await page.screenshot({ path: path.join(SHOTS, eng, 'pin-clicked.png') });

  // ── 5. CT-COPY ────────────────────────────────────────────────────────────
  // First open tray (click header to expand if collapsed)
  const ctCopyResult = await page.evaluate(() => {
    // Try to expand tray first
    const head = document.getElementById('chosen-tray-head');
    if (head) head.click();
    return null;
  });
  await page.waitForTimeout(400);
  const copyBtnResult = await page.evaluate(() => {
    const btn = document.getElementById('ct-copy');
    if (!btn) return { found: false, visible: false };
    const cs = window.getComputedStyle(btn);
    return { found: true, visible: cs.display !== 'none' && cs.visibility !== 'hidden', display: cs.display };
  });
  console.log('ct-copy state:', JSON.stringify(copyBtnResult));
  const copyClicked = await page.evaluate(() => {
    const btn = document.getElementById('ct-copy');
    if (!btn) return 'not found';
    try { btn.click(); return 'clicked'; } catch (e) { return 'error: ' + e.message; }
  });
  R('ct-copy button clickable', copyClicked === 'clicked', 'visible=' + copyBtnResult.visible + ' result=' + copyClicked);

  // ── 6. NUMBERS OVERLAY OFF ────────────────────────────────────────────────
  await page.evaluate(() => {
    const btn = document.getElementById('vr-overlay-btn');
    if (btn) btn.click();
  });
  await page.waitForTimeout(600);
  const pinsAfterOff = await page.evaluate(() => document.querySelectorAll('.el-pin').length);
  R('Numbers overlay OFF (toggle)', pinsAfterOff === 0, 'pins=' + pinsAfterOff);
  await page.screenshot({ path: path.join(SHOTS, eng, 'numbers-off.png') });

  // ── 7. YAW SPIN ──────────────────────────────────────────────────────────
  // Focus canvas then keypress
  await page.evaluate(() => {
    const canvas = document.getElementById('showroom-canvas');
    if (canvas) canvas.focus();
  });
  for (let i = 0; i < 5; i++) {
    await page.keyboard.press('ArrowLeft');
    await page.waitForTimeout(60);
  }
  await page.waitForTimeout(800);
  await page.screenshot({ path: path.join(SHOTS, eng, 'yaw-left.png') });
  const canvasOkAfterYaw = await page.evaluate(() => {
    const c = document.getElementById('showroom-canvas');
    return { exists: !!c, width: c ? c.offsetWidth : 0, noError: true };
  });
  R('Arrow-key yaw spin (5× ArrowLeft)', canvasOkAfterYaw.exists, 'canvas ' + canvasOkAfterYaw.width + 'px, no crash');
  for (let i = 0; i < 5; i++) {
    await page.keyboard.press('ArrowRight');
    await page.waitForTimeout(60);
  }

  // ── 8. WING CLICK (via dispatchEvent) ─────────────────────────────────────
  await page.screenshot({ path: path.join(SHOTS, eng, 'wing-before.png') });
  await page.evaluate(() => {
    const canvas = document.getElementById('showroom-canvas');
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;
    canvas.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }));
    canvas.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, clientX: cx, clientY: cy }));
    canvas.dispatchEvent(new MouseEvent('click',     { bubbles: true, clientX: cx, clientY: cy }));
  });
  await page.waitForTimeout(2500);
  const detailState = await page.evaluate(() => {
    const d = document.getElementById('wing-detail');
    if (!d) return { exists: false };
    const cs = window.getComputedStyle(d);
    return {
      exists: true,
      hidden: d.classList.contains('hidden'),
      collapsed: d.classList.contains('collapsed'),
      display: cs.display,
      opacity: cs.opacity,
    };
  });
  console.log('Wing detail state after click:', JSON.stringify(detailState));
  R('Wing canvas click → detail visible', detailState.exists && !detailState.hidden,
    'hidden=' + detailState.hidden + ' collapsed=' + detailState.collapsed + ' display=' + detailState.display);
  await page.screenshot({ path: path.join(SHOTS, eng, 'wing-after.png') });

  // If detail visible, test expand
  if (detailState.exists && !detailState.hidden) {
    if (detailState.collapsed) {
      await page.evaluate(() => {
        const b = document.getElementById('detail-collapse');
        if (b) b.click();
      });
      await page.waitForTimeout(800);
    }
    const expanded = await page.evaluate(() => {
      const d = document.getElementById('wing-detail');
      return d ? !d.classList.contains('collapsed') : false;
    });
    const specText = await page.evaluate(() => {
      const sp = document.querySelector('.wd-specs, .wd-body, #detail-spec-grid');
      return sp ? sp.textContent.trim().substring(0, 200) : '';
    });
    R('Wing detail expand', expanded, 'expanded=' + expanded);
    R('Wing detail specs text', specText.length > 10, '"' + specText.substring(0, 80) + '"');
    await page.screenshot({ path: path.join(SHOTS, eng, 'wing-detail-expanded.png') });

    // Close
    await page.evaluate(() => {
      const b = document.getElementById('close-detail');
      if (b) b.click();
    });
    await page.waitForTimeout(500);
    const closed = await page.evaluate(() => {
      const d = document.getElementById('wing-detail');
      return d ? d.classList.contains('hidden') : true;
    });
    R('Wing detail close', closed, 'hidden=' + closed);
  }

  // ── 9. createImageBitmap / OffscreenCanvas (WebKit) ───────────────────────
  const imgBitmapOk = await page.evaluate(() => typeof createImageBitmap === 'function');
  const offscreenOk = await page.evaluate(() => {
    try { return !!new OffscreenCanvas(2, 2); } catch (e) { return false; }
  });
  R('createImageBitmap API present', imgBitmapOk, eng === 'safari' ? 'WEBKIT-KEY' : 'chrome');
  R('OffscreenCanvas API present', offscreenOk, eng === 'safari' ? 'WEBKIT-KEY' : 'chrome');

  // ── 10. GUIDED BAR ───────────────────────────────────────────────────────
  await page.screenshot({ path: path.join(SHOTS, eng, 'guided-bar.png') });
  const guidedState = await page.evaluate(() => {
    const bar = document.getElementById('guided-bar');
    return { exists: !!bar, visible: bar ? bar.offsetWidth > 0 : false, display: bar ? window.getComputedStyle(bar).display : null };
  });
  R('Guided bar visible', guidedState.visible, 'display=' + guidedState.display);

  // Next
  await page.evaluate(() => { const b = document.getElementById('g-next'); if (b) b.click(); });
  await page.waitForTimeout(1500);
  await page.screenshot({ path: path.join(SHOTS, eng, 'guided-next.png') });
  R('Guided Next click', true, 'clicked via evaluate');

  // Prev
  await page.evaluate(() => { const b = document.getElementById('g-prev'); if (b) b.click(); });
  await page.waitForTimeout(1200);
  R('Guided Prev click', true, 'clicked via evaluate');

  // Auto-Tour
  await page.evaluate(() => { const b = document.getElementById('g-tour'); if (b) b.click(); });
  await page.waitForTimeout(2000);
  const tourPlaying = await page.evaluate(() => {
    const t = document.getElementById('g-tour');
    return t ? t.classList.contains('playing') : false;
  });
  await page.screenshot({ path: path.join(SHOTS, eng, 'auto-tour.png') });
  R('Auto-Tour playing', tourPlaying, 'playing=' + tourPlaying);
  await page.evaluate(() => { const b = document.getElementById('g-tour'); if (b) b.click(); });
  await page.waitForTimeout(800);

  // All Designs
  await page.evaluate(() => { const b = document.getElementById('g-grid'); if (b) b.click(); });
  await page.waitForTimeout(1500);
  const gridOpen = await page.evaluate(() => {
    const g = document.getElementById('grid-overlay');
    return g ? g.classList.contains('open') : false;
  });
  await page.screenshot({ path: path.join(SHOTS, eng, 'all-designs.png') });
  R('All Designs grid open', gridOpen, 'open=' + gridOpen);
  await page.evaluate(() => { const b = document.getElementById('go-close'); if (b) b.click(); });
  await page.waitForTimeout(500);

  // ── 11. SLIDERS ──────────────────────────────────────────────────────────
  for (const [slId, slName, slVal] of [['density-range','density','14'],['angle-range','angle','45'],['reveal-range','reveal','50']]) {
    const r = await page.evaluate(({id, val}) => {
      const el = document.getElementById(id);
      if (!el) return { found: false };
      const old = el.value;
      el.value = val;
      el.dispatchEvent(new Event('input', { bubbles: true }));
      return { found: true, old, newVal: el.value };
    }, {id: slId, val: slVal});
    R('Slider ' + slName, r.found, r.found ? r.old + '→' + r.newVal : 'not found');
  }
  await page.screenshot({ path: path.join(SHOTS, eng, 'sliders.png') });

  // ── 12. TOP-BAR BUTTONS ──────────────────────────────────────────────────
  for (const btnId of ['btn-bookmatch', 'btn-walls', 'btn-explore']) {
    const r = await page.evaluate((id) => {
      const b = document.getElementById(id);
      if (!b) return { found: false };
      const before = b.className;
      b.click();
      const after = b.className;
      return { found: true, visible: b.offsetWidth > 0, before, after, changed: before !== after };
    }, btnId);
    R('Top-bar ' + btnId, r.found, JSON.stringify(r));
    // Toggle back
    await page.evaluate((id) => { const b = document.getElementById(id); if (b) b.click(); }, btnId);
  }

  // ── 13. WINDOW NAV ────────────────────────────────────────────────────────
  const labelBefore = await page.evaluate(() => {
    const l = document.getElementById('window-label');
    return l ? l.textContent.trim() : null;
  });
  await page.evaluate(() => { const b = document.getElementById('btn-next-window'); if (b) b.click(); });
  await page.waitForTimeout(1800);
  const labelAfter = await page.evaluate(() => {
    const l = document.getElementById('window-label');
    return l ? l.textContent.trim() : null;
  });
  await page.screenshot({ path: path.join(SHOTS, eng, 'window-nav.png') });
  R('Window nav Next50', labelAfter !== labelBefore && labelAfter !== '1–50 of 883', 'before=' + labelBefore + ' after=' + labelAfter);
  // Prev
  await page.evaluate(() => { const b = document.getElementById('btn-prev-window'); if (b) b.click(); });
  await page.waitForTimeout(1500);
  R('Window nav Prev50', true, 'clicked');

  // ── 14. SORT SELECT ───────────────────────────────────────────────────────
  const sortOk = await page.evaluate(() => {
    const s = document.getElementById('sort-select');
    if (!s) return false;
    s.value = 'title';
    s.dispatchEvent(new Event('change', { bubbles: true }));
    return s.value === 'title';
  });
  R('Sort select', sortOk, 'value=title');

  // ── 15. PROTO PAGES ──────────────────────────────────────────────────────
  const protos = [
    { route: '/proto/v6-colorriver.html', name: 'v6-colorriver', clickSel: 'canvas, .swatch-card, button, .filter-btn' },
    { route: '/proto/v7-moodboard.html', name: 'v7-moodboard',  clickSel: 'canvas, .board-card, button' },
    { route: '/proto/v8-swipe.html',     name: 'v8-swipe',      clickSel: 'button, .card, .swipe-card, .keep-btn, .skip-btn' },
    { route: '/proto/v9-walkin.html',    name: 'v9-walkin',     clickSel: 'canvas, button, .design-chip' },
    { route: '/proto/v10-concierge.html',name: 'v10-concierge', clickSel: 'button, .send-btn, input[type="text"]' },
    { route: '/proto/sample-table.html', name: 'sample-table',  clickSel: 'canvas, button' },
  ];
  for (const proto of protos) {
    const pPage = await ctx.newPage();
    const pErrs = [];
    pPage.on('console', m => { if (m.type() === 'error') pErrs.push(m.text()); });
    pPage.on('pageerror', e => pErrs.push('PAGEERR:' + e));
    await pPage.goto('http://127.0.0.1:7690' + proto.route, { waitUntil: 'domcontentloaded', timeout: 15000 });
    await pPage.waitForTimeout(2000);
    await pPage.screenshot({ path: path.join(SHOTS, eng, proto.name + '-before.png') });

    const bodyLen = await pPage.evaluate(() => document.body.innerText.trim().length);
    const title = await pPage.title();

    // Try clicking primary element (via evaluate to avoid visibility issues)
    let primaryOk = false;
    const primaryResult = await pPage.evaluate((sel) => {
      const el = document.querySelector(sel);
      if (!el) return 'not found';
      try { el.click(); return 'clicked: ' + el.tagName + '.' + el.className.substring(0, 20); }
      catch (e) { return 'error: ' + e.message; }
    }, proto.clickSel);
    primaryOk = !primaryResult.includes('not found') && !primaryResult.includes('error');
    await pPage.waitForTimeout(800);
    await pPage.screenshot({ path: path.join(SHOTS, eng, proto.name + '-after.png') });

    console.log('Proto', proto.name, 'title=', title, 'body=', bodyLen, 'primary=', primaryResult, 'errors=', pErrs.length);
    R('Proto ' + proto.name + ' renders', bodyLen > 10, 'title="' + title + '"');
    R('Proto ' + proto.name + ' primary click', primaryOk, primaryResult);
    if (pErrs.length > 0) R('Proto ' + proto.name + ' 0 errors', false, pErrs.slice(0, 3).join('; '));

    await pPage.close();
  }

  // ── FINAL: ERROR COUNT ────────────────────────────────────────────────────
  console.log('\nTotal console errors:', errs.length);
  if (errs.length > 0) console.log('Errors:', errs.slice(0, 10));
  R('Zero console errors (main page)', errs.length === 0, 'count=' + errs.length);

  await page.screenshot({ path: path.join(SHOTS, eng, 'final.png') });
  await ctx.close();
  await browser.close();

  return results;
}

(async () => {
  const allResults = [];

  for (const [eng, launcher] of [['chrome', chromium], ['safari', webkit]]) {
    const r = await testEngine(eng, launcher);
    allResults.push(...r);
  }

  // Summary
  console.log('\n=== FINAL SUMMARY ===');
  const chromeFail = allResults.filter(r => r.eng === 'chrome' && !r.ok);
  const safariFail = allResults.filter(r => r.eng === 'safari' && !r.ok);
  const chromePass = allResults.filter(r => r.eng === 'chrome' && r.ok);
  const safariPass = allResults.filter(r => r.eng === 'safari' && r.ok);

  console.log('Chrome: PASS=' + chromePass.length + ' FAIL=' + chromeFail.length);
  console.log('Safari: PASS=' + safariPass.length + ' FAIL=' + safariFail.length);

  if (chromeFail.length) {
    console.log('\nChrome FAIL:');
    chromeFail.forEach(r => console.log('  -', r.comp, '|', r.note));
  }
  if (safariFail.length) {
    console.log('\nSafari FAIL:');
    safariFail.forEach(r => console.log('  -', r.comp, '|', r.note));
  }

  // Mismatches
  const allComps = [...new Set(allResults.map(r => r.comp))];
  const mismatches = [];
  for (const comp of allComps) {
    const cr = allResults.find(r => r.eng === 'chrome' && r.comp === comp);
    const sa = allResults.find(r => r.eng === 'safari' && r.comp === comp);
    if (cr && sa && cr.ok !== sa.ok) mismatches.push({ comp, chrome: cr.ok, safari: sa.ok });
  }
  if (mismatches.length) {
    console.log('\nChrome/Safari MISMATCHES:');
    mismatches.forEach(m => console.log('  -', m.comp, 'chrome=' + m.chrome, 'safari=' + m.safari));
  } else {
    console.log('\nNo Chrome/Safari mismatches.');
  }

  // Save JSON results
  fs.writeFileSync('/Users/macstudio3/Projects/quadrille-showroom/cta/targeted-results.json', JSON.stringify(allResults, null, 2));
  console.log('\nResults saved to cta/targeted-results.json');
})().catch(e => { console.error('FATAL:', e); process.exit(1); });