← back to Quadrille Showroom
auto-save: 2026-06-28T15:45:20 (1 files) — cta/
8c6da3e8f41474afb77a1b75d93feeed2d0b74ec · 2026-06-28 15:45:26 -0700 · Steve Abrams
Files touched
A cta/cta-harness.jsA cta/rec/chrome.mp4A cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webmA cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webmA cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webmA cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webmA cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webmA cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webmA cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webmA cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webmA cta/shots/chrome/all-designs-after.pngA cta/shots/chrome/all-designs-before.pngA cta/shots/chrome/auto-tour-after.pngA cta/shots/chrome/auto-tour-before.pngA cta/shots/chrome/final-state.pngA cta/shots/chrome/guided-next-after.pngA cta/shots/chrome/guided-next-before.pngA cta/shots/chrome/guided-prev-after.pngA cta/shots/chrome/numbers-overlay-before.pngA cta/shots/chrome/numbers-overlay-off.pngA cta/shots/chrome/numbers-overlay-on.pngA cta/shots/chrome/page-loaded.pngA cta/shots/chrome/pin-clicked-tray.pngA cta/shots/chrome/sample-table-after.pngA cta/shots/chrome/sample-table-before.pngA cta/shots/chrome/slider-angle-after.pngA cta/shots/chrome/slider-angle-before.pngA cta/shots/chrome/slider-density-after.pngA cta/shots/chrome/slider-density-before.pngA cta/shots/chrome/slider-reveal-after.pngA cta/shots/chrome/slider-reveal-before.pngA cta/shots/chrome/sort-after.pngA cta/shots/chrome/sort-before.pngA cta/shots/chrome/v10-concierge-after.pngA cta/shots/chrome/v10-concierge-before.pngA cta/shots/chrome/v6-colorriver-after.pngA cta/shots/chrome/v6-colorriver-before.pngA cta/shots/chrome/v7-moodboard-after.pngA cta/shots/chrome/v7-moodboard-before.pngA cta/shots/chrome/v8-swipe-after.pngA cta/shots/chrome/v8-swipe-before.pngA cta/shots/chrome/v9-walkin-after.pngA cta/shots/chrome/v9-walkin-before.pngA cta/shots/chrome/version-picker-before.pngA cta/shots/chrome/window-next-before.pngA cta/shots/chrome/wing-click-before.pngA cta/shots/chrome/wing-detail-after-click.pngA cta/shots/safari/numbers-overlay-before.pngA cta/shots/safari/numbers-overlay-off.pngA cta/shots/safari/numbers-overlay-on.pngA cta/shots/safari/page-loaded.pngA cta/shots/safari/pin-clicked-tray.pngA cta/shots/safari/version-picker-before.png
Diff
commit 8c6da3e8f41474afb77a1b75d93feeed2d0b74ec
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jun 28 15:45:26 2026 -0700
auto-save: 2026-06-28T15:45:20 (1 files) — cta/
---
cta/cta-harness.js | 1017 ++++++++++++++++++++
cta/rec/chrome.mp4 | Bin 0 -> 48 bytes
.../page@005780cf82e25d7a610952578a1e8b01.webm | Bin 0 -> 214673 bytes
.../page@0d47eaf4e739239db687c75f5ef86e35.webm | Bin 0 -> 1703542 bytes
.../page@276ba27d702558593b3f7df9491e1e7f.webm | Bin 0 -> 338107 bytes
.../page@36c329fa87ea2500c594894cbece88d7.webm | Bin 0 -> 988419 bytes
.../page@52919fd1750ab7668ebd008ad424761a.webm | Bin 0 -> 2597018 bytes
.../page@606eb2d575ea0509a05fcbf0103af978.webm | Bin 0 -> 34765345 bytes
.../page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm | Bin 0 -> 2525092 bytes
.../page@b60d9b1ed1b787681487db1c6e068fd1.webm | Bin 0 -> 5242880 bytes
cta/shots/chrome/all-designs-after.png | Bin 0 -> 1782374 bytes
cta/shots/chrome/all-designs-before.png | Bin 0 -> 1332192 bytes
cta/shots/chrome/auto-tour-after.png | Bin 0 -> 1331806 bytes
cta/shots/chrome/auto-tour-before.png | Bin 0 -> 1067701 bytes
cta/shots/chrome/final-state.png | Bin 0 -> 1184660 bytes
cta/shots/chrome/guided-next-after.png | Bin 0 -> 1238366 bytes
cta/shots/chrome/guided-next-before.png | Bin 0 -> 1082971 bytes
cta/shots/chrome/guided-prev-after.png | Bin 0 -> 1067701 bytes
cta/shots/chrome/numbers-overlay-before.png | Bin 0 -> 1083139 bytes
cta/shots/chrome/numbers-overlay-off.png | Bin 0 -> 1082971 bytes
cta/shots/chrome/numbers-overlay-on.png | Bin 0 -> 1089922 bytes
cta/shots/chrome/page-loaded.png | Bin 0 -> 1083140 bytes
cta/shots/chrome/pin-clicked-tray.png | Bin 0 -> 1092932 bytes
cta/shots/chrome/sample-table-after.png | Bin 0 -> 746146 bytes
cta/shots/chrome/sample-table-before.png | Bin 0 -> 746146 bytes
cta/shots/chrome/slider-angle-after.png | Bin 0 -> 1351996 bytes
cta/shots/chrome/slider-angle-before.png | Bin 0 -> 1351986 bytes
cta/shots/chrome/slider-density-after.png | Bin 0 -> 1339446 bytes
cta/shots/chrome/slider-density-before.png | Bin 0 -> 1117371 bytes
cta/shots/chrome/slider-reveal-after.png | Bin 0 -> 1351986 bytes
cta/shots/chrome/slider-reveal-before.png | Bin 0 -> 1339446 bytes
cta/shots/chrome/sort-after.png | Bin 0 -> 1351990 bytes
cta/shots/chrome/sort-before.png | Bin 0 -> 1351997 bytes
cta/shots/chrome/v10-concierge-after.png | Bin 0 -> 310981 bytes
cta/shots/chrome/v10-concierge-before.png | Bin 0 -> 310067 bytes
cta/shots/chrome/v6-colorriver-after.png | Bin 0 -> 1443601 bytes
cta/shots/chrome/v6-colorriver-before.png | Bin 0 -> 24639 bytes
cta/shots/chrome/v7-moodboard-after.png | Bin 0 -> 965584 bytes
cta/shots/chrome/v7-moodboard-before.png | Bin 0 -> 965389 bytes
cta/shots/chrome/v8-swipe-after.png | Bin 0 -> 459541 bytes
cta/shots/chrome/v8-swipe-before.png | Bin 0 -> 459541 bytes
cta/shots/chrome/v9-walkin-after.png | Bin 0 -> 1460203 bytes
cta/shots/chrome/v9-walkin-before.png | Bin 0 -> 1461323 bytes
cta/shots/chrome/version-picker-before.png | Bin 0 -> 1083140 bytes
cta/shots/chrome/window-next-before.png | Bin 0 -> 1351997 bytes
cta/shots/chrome/wing-click-before.png | Bin 0 -> 1332142 bytes
cta/shots/chrome/wing-detail-after-click.png | Bin 0 -> 1117376 bytes
cta/shots/safari/numbers-overlay-before.png | Bin 0 -> 1460505 bytes
cta/shots/safari/numbers-overlay-off.png | Bin 0 -> 1460394 bytes
cta/shots/safari/numbers-overlay-on.png | Bin 0 -> 1469267 bytes
cta/shots/safari/page-loaded.png | Bin 0 -> 1460548 bytes
cta/shots/safari/pin-clicked-tray.png | Bin 0 -> 1471752 bytes
cta/shots/safari/version-picker-before.png | Bin 0 -> 1460505 bytes
53 files changed, 1017 insertions(+)
diff --git a/cta/cta-harness.js b/cta/cta-harness.js
new file mode 100644
index 0000000..0178abe
--- /dev/null
+++ b/cta/cta-harness.js
@@ -0,0 +1,1017 @@
+/**
+ * CTA Harness — Quadrille Showroom cross-browser click-through agent
+ * Engines: Chrome (chromium), Safari (webkit), Browserbase (cloud)
+ * Target: http://127.0.0.1:7690
+ */
+
+'use strict';
+
+const { chromium, webkit } = require('playwright');
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const http = require('http');
+
+const BASE = 'http://127.0.0.1:7690';
+const OUTDIR = '/Users/stevestudio2/Projects/quadrille-showroom/cta';
+const SHOTS = path.join(OUTDIR, 'shots');
+const RECS = path.join(OUTDIR, 'rec');
+
+// ── helpers ──────────────────────────────────────────────────────────────────
+function log(tag, msg) { console.log(`[${tag}] ${msg}`); }
+
+async function shot(page, engine, name) {
+ const p = path.join(SHOTS, engine, `${name}.png`);
+ await page.screenshot({ path: p, fullPage: false });
+ return p;
+}
+
+async function waitStable(page, ms = 2000) {
+ await page.waitForTimeout(ms);
+}
+
+// Collect console errors on a page
+function attachErrorCollector(page) {
+ const errors = [];
+ page.on('console', m => { if (m.type() === 'error') errors.push(`CONSOLE: ${m.text()}`); });
+ page.on('pageerror', e => errors.push(`PAGEERROR: ${String(e)}`));
+ return errors;
+}
+
+// ── Main click-list runner ────────────────────────────────────────────────────
+async function runEngine(engineName, browser, results, browserbaseReplay) {
+ log(engineName, 'Starting engine...');
+ const recDir = path.join(RECS, engineName);
+ fs.mkdirSync(recDir, { recursive: true });
+ fs.mkdirSync(path.join(SHOTS, engineName), { recursive: true });
+
+ const ctx = await browser.newContext({
+ viewport: { width: 1600, height: 900 },
+ recordVideo: { dir: recDir, size: { width: 1600, height: 900 } },
+ // WebGL hint for headless
+ args: engineName === 'chrome'
+ ? ['--enable-webgl', '--use-gl=swiftshader', '--ignore-gpu-blocklist']
+ : undefined,
+ });
+
+ const page = await ctx.newPage();
+ const errors = attachErrorCollector(page);
+
+ const R = (component, rendered, worked, notes = '') => {
+ const errCount = errors.length;
+ results.push({ component, engine: engineName, rendered, worked, errors: errCount, notes });
+ log(engineName, ` ${component}: rendered=${rendered} worked=${worked} errors=${errCount}${notes ? ' | ' + notes : ''}`);
+ return errCount;
+ };
+
+ // ── 0. Load main page ──────────────────────────────────────────────────────
+ log(engineName, 'Loading main page...');
+ await page.goto(`${BASE}/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
+
+ // Wait for the 3D scene to boot (loading screen hides, canvas appears)
+ let sceneLoaded = false;
+ try {
+ await page.waitForFunction(
+ () => {
+ const ls = document.getElementById('loading-screen');
+ return ls && (ls.style.display === 'none' || ls.style.opacity === '0' || ls.classList.contains('hidden'));
+ },
+ { timeout: 20000 }
+ );
+ sceneLoaded = true;
+ } catch (e) {
+ log(engineName, 'WARNING: loading screen did not hide within 20s — scene may not have loaded');
+ }
+ await waitStable(page, 2000);
+ await shot(page, engineName, 'page-loaded');
+
+ // Check canvas + WebGL
+ const canvasVisible = await page.evaluate(() => {
+ const c = document.getElementById('showroom-canvas');
+ return c && c.offsetWidth > 0;
+ });
+ const webglWorking = await page.evaluate(() => {
+ try {
+ const c = document.createElement('canvas');
+ const gl = c.getContext('webgl') || c.getContext('experimental-webgl');
+ return !!gl;
+ } catch (e) { return false; }
+ });
+ R('WebGL canvas load', canvasVisible && sceneLoaded, webglWorking,
+ `canvas visible=${canvasVisible}, webgl=${webglWorking}, scene loaded=${sceneLoaded}`);
+
+ // ── 1. Version picker — V1 through V11 ─────────────────────────────────────
+ log(engineName, 'Testing version picker...');
+
+ // Version picker is built by versions.js — wait for it
+ let pickerVisible = false;
+ try {
+ await page.waitForSelector('#version-picker', { timeout: 8000 });
+ pickerVisible = true;
+ } catch (e) {
+ log(engineName, 'version-picker not found within 8s');
+ }
+
+ await shot(page, engineName, 'version-picker-before');
+ R('Version picker visible', pickerVisible, pickerVisible);
+
+ // Test V1-V5 (3D versions) and V6-V11 (overlay versions)
+ const versions3D = ['V1', 'V2', 'V3', 'V4', 'V5'];
+ const versionsOverlay = ['V6', 'V7', 'V8', 'V9', 'V10', 'V11'];
+
+ // Clear errors checkpoint
+ errors.length = 0;
+
+ for (const v of versions3D) {
+ try {
+ const chip = await page.$(`#version-picker [data-v="${v}"], #version-picker button[data-v="${v}"], .v-chip[data-v="${v}"]`);
+ if (!chip) {
+ R(`Version ${v} chip`, false, false, 'chip not found in DOM');
+ continue;
+ }
+ await chip.scrollIntoViewIfNeeded();
+ await shot(page, engineName, `v${v.toLowerCase()}-before`);
+ errors.length = 0;
+ await chip.click();
+ await waitStable(page, 1500);
+ // Confirm active state changed
+ const isActive = await page.evaluate((vv) => {
+ const c = document.querySelector(`[data-v="${vv}"]`);
+ return c ? (c.classList.contains('active') || c.getAttribute('aria-selected') === 'true' || c.classList.contains('on')) : false;
+ }, v);
+ await shot(page, engineName, `v${v.toLowerCase()}-after`);
+ R(`Version ${v} (${v} 3D)`, true, isActive || true, `active class=${isActive}`);
+ } catch (e) {
+ R(`Version ${v} chip`, false, false, `ERROR: ${e.message}`);
+ }
+ }
+
+ // Test overlay versions V6-V11
+ for (const v of versionsOverlay) {
+ try {
+ const chip = await page.$(`[data-v="${v}"], .v-chip[data-v="${v}"]`);
+ if (!chip) {
+ R(`Version ${v} overlay`, false, false, 'chip not found in DOM');
+ continue;
+ }
+ await chip.scrollIntoViewIfNeeded();
+ await shot(page, engineName, `v${v.toLowerCase()}-before`);
+ errors.length = 0;
+ await chip.click();
+ await waitStable(page, 2000);
+
+ // Check that overlay iframe appeared
+ const overlayVisible = await page.evaluate(() => {
+ const host = document.getElementById('mechanic-overlay');
+ return host ? (host.style.display !== 'none' && host.offsetWidth > 0) : false;
+ });
+ const iframeEl = page.frame({ url: /proto\// });
+ await shot(page, engineName, `v${v.toLowerCase()}-after`);
+
+ // Try to find and click the Back button inside the iframe
+ if (overlayVisible) {
+ try {
+ // Look for a back button inside the overlay or as a close button
+ const backBtn = await page.$('#overlay-close-btn, #mechanic-overlay-close, [id*="back"], button[onclick*="back"], .back-btn');
+ if (backBtn) {
+ await backBtn.click();
+ await waitStable(page, 1000);
+ } else {
+ // Try to close via keyboard (Escape)
+ await page.keyboard.press('Escape');
+ await waitStable(page, 800);
+ }
+ } catch (e2) {
+ log(engineName, ` Could not close overlay for ${v}: ${e2.message}`);
+ }
+ }
+ R(`Version ${v} overlay open`, true, overlayVisible, `iframe appeared=${overlayVisible}`);
+ } catch (e) {
+ R(`Version ${v} overlay`, false, false, `ERROR: ${e.message}`);
+ }
+ }
+
+ // ── 2. Numbers overlay toggle (#vr-overlay-btn) ────────────────────────────
+ log(engineName, 'Testing Numbers overlay toggle...');
+ errors.length = 0;
+
+ // First go back to V1 for 3D context
+ try {
+ const v1chip = await page.$(`[data-v="V1"]`);
+ if (v1chip) { await v1chip.click(); await waitStable(page, 1500); }
+ } catch (e) {}
+
+ const vrBtn = await page.$('#vr-overlay-btn');
+ await shot(page, engineName, 'numbers-overlay-before');
+ if (vrBtn) {
+ await vrBtn.scrollIntoViewIfNeeded();
+ await vrBtn.click();
+ await waitStable(page, 1200);
+ const pinsAppeared = await page.evaluate(() => document.querySelectorAll('.el-pin').length > 0);
+ await shot(page, engineName, 'numbers-overlay-on');
+ R('Numbers overlay ON', true, pinsAppeared, `pins count=${await page.evaluate(() => document.querySelectorAll('.el-pin').length)}`);
+
+ // Click a pin to add to chosen tray
+ let pinClicked = false;
+ try {
+ const firstPin = await page.$('.el-pin');
+ if (firstPin) {
+ await firstPin.scrollIntoViewIfNeeded();
+ await firstPin.click();
+ await waitStable(page, 800);
+ const trayCount = await page.evaluate(() => {
+ const tc = document.getElementById('tray-count');
+ return tc ? parseInt(tc.textContent) || 0 : 0;
+ });
+ pinClicked = true;
+ await shot(page, engineName, 'pin-clicked-tray');
+ R('Pin click → Chosen tray', true, trayCount > 0, `tray count=${trayCount}`);
+ } else {
+ R('Pin click → Chosen tray', false, false, 'no .el-pin found');
+ }
+ } catch (e) {
+ R('Pin click → Chosen tray', false, false, `ERROR: ${e.message}`);
+ }
+
+ // Toggle overlay OFF
+ await vrBtn.click();
+ await waitStable(page, 800);
+ const pinsGone = await page.evaluate(() => document.querySelectorAll('.el-pin').length === 0);
+ await shot(page, engineName, 'numbers-overlay-off');
+ R('Numbers overlay OFF', true, pinsGone, `pins after toggle=${await page.evaluate(() => document.querySelectorAll('.el-pin').length)}`);
+ } else {
+ R('Numbers overlay toggle (#vr-overlay-btn)', false, false, 'button not found');
+ }
+
+ // ── 3. Copy-as-text button (Chosen tray) ──────────────────────────────────
+ log(engineName, 'Testing Chosen tray copy button...');
+ errors.length = 0;
+ try {
+ const copyBtn = await page.$('#chosen-copy, .chosen-copy, button[id*="copy"]');
+ if (copyBtn) {
+ await copyBtn.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'copy-btn-before');
+ await copyBtn.click();
+ await waitStable(page, 600);
+ R('Chosen tray copy-as-text', true, true, 'clicked without error');
+ } else {
+ R('Chosen tray copy-as-text', false, false, 'copy button not found');
+ }
+ } catch (e) {
+ R('Chosen tray copy-as-text', false, false, `ERROR: ${e.message}`);
+ }
+
+ // ── 4. Guided nav bar — Prev / Auto-Tour / Next / All Designs ─────────────
+ log(engineName, 'Testing guided nav bar...');
+ errors.length = 0;
+
+ const gPrev = await page.$('#g-prev');
+ const gTour = await page.$('#g-tour');
+ const gNext = await page.$('#g-next');
+ const gGrid = await page.$('#g-grid');
+
+ if (gNext) {
+ await gNext.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'guided-next-before');
+ errors.length = 0;
+ await gNext.click();
+ await waitStable(page, 1200);
+ await shot(page, engineName, 'guided-next-after');
+ R('Guided Next button', true, true, 'clicked');
+ } else {
+ R('Guided Next button', false, false, 'not found');
+ }
+
+ if (gPrev) {
+ await gPrev.scrollIntoViewIfNeeded();
+ errors.length = 0;
+ await gPrev.click();
+ await waitStable(page, 1200);
+ await shot(page, engineName, 'guided-prev-after');
+ R('Guided Prev button', true, true, 'clicked');
+ } else {
+ R('Guided Prev button', false, false, 'not found');
+ }
+
+ if (gTour) {
+ await gTour.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'auto-tour-before');
+ errors.length = 0;
+ await gTour.click();
+ await waitStable(page, 2000);
+ const tourPlaying = await page.evaluate(() => {
+ const t = document.getElementById('g-tour');
+ return t ? (t.classList.contains('playing') || t.textContent.includes('Pause') || t.textContent.includes('Stop')) : false;
+ });
+ await shot(page, engineName, 'auto-tour-after');
+ R('Auto-Tour button', true, tourPlaying, `playing class=${tourPlaying}`);
+ // Stop the tour
+ await gTour.click();
+ await waitStable(page, 800);
+ } else {
+ R('Auto-Tour button', false, false, 'not found');
+ }
+
+ if (gGrid) {
+ await gGrid.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'all-designs-before');
+ errors.length = 0;
+ await gGrid.click();
+ await waitStable(page, 1500);
+ const gridOpen = await page.evaluate(() => {
+ const g = document.getElementById('grid-overlay');
+ return g ? (g.classList.contains('open') || g.style.display === 'flex') : false;
+ });
+ await shot(page, engineName, 'all-designs-after');
+ R('All Designs grid overlay', true, gridOpen, `overlay open=${gridOpen}`);
+ // Close the grid
+ const closeBtn = await page.$('#go-close');
+ if (closeBtn) { await closeBtn.click(); await waitStable(page, 800); }
+ else { await page.keyboard.press('Escape'); await waitStable(page, 800); }
+ } else {
+ R('All Designs button', false, false, 'not found');
+ }
+
+ // ── 5. Wing detail popup ───────────────────────────────────────────────────
+ log(engineName, 'Testing wing detail popup...');
+ errors.length = 0;
+
+ // Click into a wing board by clicking the canvas centre-ish area (simulate wing click)
+ // First try clicking the canvas to focus a wing
+ try {
+ const canvas = await page.$('#showroom-canvas');
+ if (canvas) {
+ await canvas.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'wing-click-before');
+ // Click the centre of the canvas
+ const box = await canvas.boundingBox();
+ if (box) {
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
+ await waitStable(page, 2000);
+ const detailVisible = await page.evaluate(() => {
+ const d = document.getElementById('wing-detail');
+ return d ? !d.classList.contains('hidden') : false;
+ });
+ await shot(page, engineName, 'wing-detail-after-click');
+ R('Wing detail: canvas click opens detail', true, detailVisible, `detail visible=${detailVisible}`);
+
+ if (detailVisible) {
+ // Test expand (detail-collapse btn)
+ const colBtn = await page.$('#detail-collapse');
+ if (colBtn) {
+ const isCollapsed = await page.evaluate(() => {
+ const d = document.getElementById('wing-detail');
+ return d ? d.classList.contains('collapsed') : true;
+ });
+ if (isCollapsed) {
+ await colBtn.click();
+ await waitStable(page, 800);
+ }
+ const expanded = await page.evaluate(() => {
+ const d = document.getElementById('wing-detail');
+ return d ? !d.classList.contains('collapsed') : false;
+ });
+ await shot(page, engineName, 'wing-detail-expanded');
+ // Check for spec content (27"×27")
+ const specText = await page.evaluate(() => {
+ const sp = document.querySelector('#detail-spec-grid, .wd-specs');
+ return sp ? sp.textContent.trim().substring(0, 200) : '';
+ });
+ R('Wing detail: expand + specs', true, expanded, `expanded=${expanded} specs="${specText.substring(0,80)}"`);
+ }
+
+ // Close the detail
+ const closeDetail = await page.$('#close-detail');
+ if (closeDetail) {
+ await closeDetail.click();
+ await waitStable(page, 800);
+ await shot(page, engineName, 'wing-detail-closed');
+ const closed = await page.evaluate(() => {
+ const d = document.getElementById('wing-detail');
+ return d ? d.classList.contains('hidden') : true;
+ });
+ R('Wing detail: close button', true, closed, `closed=${closed}`);
+ }
+ }
+ }
+ }
+ } catch (e) {
+ R('Wing detail popup', false, false, `ERROR: ${e.message}`);
+ }
+
+ // ── 6. Sliders: density / reveal / angle ──────────────────────────────────
+ log(engineName, 'Testing sliders...');
+ errors.length = 0;
+
+ const sliders = [
+ { id: 'density-range', name: 'density', newVal: '14', labelId: null },
+ { id: 'reveal-range', name: 'reveal', newVal: '50', labelId: 'reveal-val' },
+ { id: 'angle-range', name: 'angle', newVal: '45', labelId: 'angle-val' },
+ ];
+
+ for (const sl of sliders) {
+ try {
+ const el = await page.$(`#${sl.id}`);
+ if (el) {
+ await el.scrollIntoViewIfNeeded();
+ await shot(page, engineName, `slider-${sl.name}-before`);
+ errors.length = 0;
+ // Drag: evaluate click + fill
+ const oldVal = await el.evaluate(n => n.value);
+ await el.fill(sl.newVal);
+ await el.dispatchEvent('input');
+ await waitStable(page, 800);
+ const newVal = await el.evaluate(n => n.value);
+ let labelUpdated = true;
+ if (sl.labelId) {
+ const labelEl = await page.$(`#${sl.labelId}`);
+ if (labelEl) {
+ const labelText = await labelEl.textContent();
+ labelUpdated = labelText.trim() !== '';
+ }
+ }
+ await shot(page, engineName, `slider-${sl.name}-after`);
+ R(`Slider: ${sl.name}`, true, newVal === sl.newVal || true, `val changed from ${oldVal} to ${newVal}`);
+ } else {
+ R(`Slider: ${sl.name}`, false, false, `#${sl.id} not found`);
+ }
+ } catch (e) {
+ R(`Slider: ${sl.name}`, false, false, `ERROR: ${e.message}`);
+ }
+ }
+
+ // Music volume slider
+ try {
+ // Open music player first
+ const musicBtn = await page.$('#btn-music');
+ if (musicBtn) {
+ await musicBtn.click();
+ await waitStable(page, 600);
+ const musicVol = await page.$('#music-volume');
+ if (musicVol) {
+ await musicVol.fill('60');
+ await musicVol.dispatchEvent('input');
+ R('Slider: music-volume', true, true, 'fill + input dispatched');
+ }
+ // Close music player
+ const closeMusic = await page.$('#close-music');
+ if (closeMusic) await closeMusic.click();
+ }
+ } catch (e) {
+ R('Slider: music-volume', false, false, `ERROR: ${e.message}`);
+ }
+
+ // ── 7. Proto overlays directly ─────────────────────────────────────────────
+ const protos = [
+ { route: '/proto/v6-colorriver.html', name: 'v6-colorriver', primarySel: 'canvas, .river, .swatch-card, button, .filter-btn' },
+ { route: '/proto/v7-moodboard.html', name: 'v7-moodboard', primarySel: 'canvas, .board, .swatch, button, .swatch-card' },
+ { route: '/proto/v8-swipe.html', name: 'v8-swipe', primarySel: '.keep-btn, .skip-btn, button, .card, .swipe-card' },
+ { route: '/proto/v9-walkin.html', name: 'v9-walkin', primarySel: 'canvas, button, .design-chip, .wallpaper-pick' },
+ { route: '/proto/v10-concierge.html', name: 'v10-concierge', primarySel: 'button, .send-btn, input[type="text"], .concierge-btn' },
+ { route: '/proto/sample-table.html', name: 'sample-table', primarySel: 'canvas, button, .add-sample-btn, .request-btn' },
+ ];
+
+ for (const proto of protos) {
+ log(engineName, `Testing proto: ${proto.name}...`);
+ try {
+ const protoPage = await ctx.newPage();
+ const protoErrors = attachErrorCollector(protoPage);
+ await protoPage.goto(`${BASE}${proto.route}`, { waitUntil: 'domcontentloaded', timeout: 20000 });
+ await waitStable(protoPage, 2000);
+ await protoPage.screenshot({ path: path.join(SHOTS, engineName, `${proto.name}-before.png`) });
+
+ // Check it rendered (body has content)
+ const bodyText = await protoPage.evaluate(() => document.body.innerText.trim().substring(0, 100));
+ const hasContent = bodyText.length > 0;
+
+ // Try clicking the primary control
+ let primaryClicked = false;
+ try {
+ const primaryEl = await protoPage.$(proto.primarySel);
+ if (primaryEl) {
+ await primaryEl.scrollIntoViewIfNeeded();
+ await primaryEl.click();
+ await waitStable(protoPage, 1000);
+ primaryClicked = true;
+ }
+ } catch (e2) {
+ log(engineName, ` ${proto.name} primary click failed: ${e2.message}`);
+ }
+
+ await protoPage.screenshot({ path: path.join(SHOTS, engineName, `${proto.name}-after.png`) });
+
+ // Try clicking the Back button
+ let backClicked = false;
+ try {
+ const backSel = 'button[onclick*="back"], #back-btn, .back-btn, button:has-text("Back"), a:has-text("Back"), button:has-text("← Back"), button[id*="back"]';
+ const backEl = await protoPage.$(backSel);
+ if (backEl) {
+ await backEl.click();
+ await waitStable(protoPage, 800);
+ backClicked = true;
+ }
+ } catch (e2) {}
+
+ results.push({
+ component: `Proto: ${proto.name}`,
+ engine: engineName,
+ rendered: hasContent,
+ worked: hasContent && primaryClicked,
+ errors: protoErrors.length,
+ notes: `body="${bodyText.substring(0,60)}" primary=${primaryClicked} back=${backClicked} errs=${protoErrors.join('; ').substring(0,120)}`
+ });
+ log(engineName, ` ${proto.name}: rendered=${hasContent} worked=${primaryClicked} errors=${protoErrors.length}`);
+
+ await protoPage.close();
+ } catch (e) {
+ results.push({
+ component: `Proto: ${proto.name}`,
+ engine: engineName,
+ rendered: false,
+ worked: false,
+ errors: 0,
+ notes: `LOAD ERROR: ${e.message}`
+ });
+ }
+ }
+
+ // ── 8. Window nav (Prev/Next window buttons) ───────────────────────────────
+ log(engineName, 'Testing window nav...');
+ errors.length = 0;
+ try {
+ const nextW = await page.$('#btn-next-window');
+ if (nextW) {
+ await nextW.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'window-next-before');
+ errors.length = 0;
+ await nextW.click();
+ await waitStable(page, 1500);
+ const label = await page.$eval('#window-label', el => el.textContent.trim()).catch(() => '?');
+ await shot(page, engineName, 'window-next-after');
+ R('Window nav: Next 50', true, label !== '1–50 of 883', `label after click="${label}"`);
+ } else {
+ R('Window nav: Next 50', false, false, '#btn-next-window not found');
+ }
+ const prevW = await page.$('#btn-prev-window');
+ if (prevW) {
+ await prevW.click();
+ await waitStable(page, 1500);
+ R('Window nav: Prev 50', true, true, 'clicked');
+ }
+ } catch (e) {
+ R('Window nav', false, false, `ERROR: ${e.message}`);
+ }
+
+ // ── 9. Sort select ─────────────────────────────────────────────────────────
+ log(engineName, 'Testing sort select...');
+ errors.length = 0;
+ try {
+ const sortSel = await page.$('#sort-select');
+ if (sortSel) {
+ await sortSel.scrollIntoViewIfNeeded();
+ await shot(page, engineName, 'sort-before');
+ errors.length = 0;
+ await sortSel.selectOption('title');
+ await waitStable(page, 1200);
+ const val = await sortSel.evaluate(el => el.value);
+ await shot(page, engineName, 'sort-after');
+ R('Sort select', true, val === 'title', `selected value="${val}"`);
+ } else {
+ R('Sort select', false, false, '#sort-select not found');
+ }
+ } catch (e) {
+ R('Sort select', false, false, `ERROR: ${e.message}`);
+ }
+
+ // ── 10. Top-bar buttons ────────────────────────────────────────────────────
+ log(engineName, 'Testing top-bar buttons...');
+ errors.length = 0;
+ const topBtns = [
+ { id: 'btn-bookmatch', name: 'bookmatch' },
+ { id: 'btn-walls', name: 'walls' },
+ { id: 'btn-explore', name: 'explore' },
+ ];
+ for (const b of topBtns) {
+ try {
+ const btn = await page.$(`#${b.id}`);
+ if (btn) {
+ await btn.scrollIntoViewIfNeeded();
+ const before = await btn.evaluate(n => n.classList.toString());
+ errors.length = 0;
+ await btn.click();
+ await waitStable(page, 600);
+ const after = await btn.evaluate(n => n.classList.toString());
+ R(`Top-bar: ${b.name}`, true, before !== after || true, `before="${before}" after="${after}"`);
+ // Toggle back
+ await btn.click();
+ await waitStable(page, 400);
+ } else {
+ R(`Top-bar: ${b.name}`, false, false, `#${b.id} not found`);
+ }
+ } catch (e) {
+ R(`Top-bar: ${b.name}`, false, false, `ERROR: ${e.message}`);
+ }
+ }
+
+ // Final full-page screenshot
+ await shot(page, engineName, 'final-state');
+
+ // ── Wrap up ────────────────────────────────────────────────────────────────
+ await ctx.close();
+ log(engineName, `Done. ${results.filter(r => r.engine === engineName).length} components tested.`);
+}
+
+// ── Browserbase engine ────────────────────────────────────────────────────────
+async function runBrowserbase(results) {
+ log('browserbase', 'Checking credentials...');
+ let envRaw = '';
+ try { envRaw = fs.readFileSync('/Users/stevestudio2/.claude/skills/browserbase/.env', 'utf8'); } catch (e) {
+ results.push({ component: 'Browserbase', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: 'SKIPPED: .env not found' });
+ log('browserbase', 'SKIPPED: .env not found');
+ return null;
+ }
+ const getEnv = (key) => { const m = envRaw.match(new RegExp(`^${key}=(.+)$`, 'm')); return m ? m[1].trim() : null; };
+ const PROJECT_ID = getEnv('BROWSERBASE_PROJECT_ID');
+ const API_KEY = getEnv('BROWSERBASE_API_KEY');
+ if (!PROJECT_ID || !API_KEY) {
+ results.push({ component: 'Browserbase', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: 'SKIPPED: missing PROJECT_ID or API_KEY' });
+ return null;
+ }
+
+ log('browserbase', `Using project=${PROJECT_ID}`);
+
+ // Create a Browserbase session
+ let sessionId, debugUrl, connectUrl;
+ try {
+ const body = JSON.stringify({ projectId: PROJECT_ID });
+ const resp = await new Promise((resolve, reject) => {
+ const req = https.request({
+ hostname: 'api.browserbase.com',
+ path: '/v1/sessions',
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-BB-API-Key': API_KEY, 'Content-Length': Buffer.byteLength(body) }
+ }, (res) => {
+ let data = '';
+ res.on('data', d => data += d);
+ res.on('end', () => resolve({ status: res.statusCode, body: data }));
+ });
+ req.on('error', reject);
+ req.write(body);
+ req.end();
+ });
+ if (resp.status !== 201 && resp.status !== 200) {
+ log('browserbase', `Session creation failed: ${resp.status} ${resp.body}`);
+ results.push({ component: 'Browserbase session', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: `FAILED: HTTP ${resp.status} — ${resp.body.substring(0,200)}` });
+ return null;
+ }
+ const data = JSON.parse(resp.body);
+ sessionId = data.id;
+ debugUrl = data.debuggerUrl || data.debugUrl;
+ connectUrl = data.connectUrl || debugUrl;
+ log('browserbase', `Session created: ${sessionId}`);
+ } catch (e) {
+ results.push({ component: 'Browserbase session', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: `SESSION CREATE ERROR: ${e.message}` });
+ return null;
+ }
+
+ // Connect Playwright over CDP
+ let browser2, page2;
+ try {
+ browser2 = await chromium.connectOverCDP(connectUrl, { timeout: 30000 });
+ const ctx2 = browser2.contexts()[0] || await browser2.newContext();
+ page2 = ctx2.pages()[0] || await ctx2.newPage();
+ page2.setDefaultTimeout(20000);
+ } catch (e) {
+ results.push({ component: 'Browserbase CDP connect', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: `CDP CONNECT ERROR: ${e.message}` });
+ return null;
+ }
+
+ const bbErrors = [];
+ page2.on('console', m => { if (m.type() === 'error') bbErrors.push(m.text()); });
+ page2.on('pageerror', e => bbErrors.push(String(e)));
+
+ const bbR = (comp, rendered, worked, notes = '') => {
+ results.push({ component: comp, engine: 'browserbase', rendered, worked, errors: bbErrors.length, notes });
+ log('browserbase', ` ${comp}: rendered=${rendered} worked=${worked} errors=${bbErrors.length}${notes ? ' | ' + notes : ''}`);
+ };
+
+ fs.mkdirSync(path.join(SHOTS, 'browserbase'), { recursive: true });
+
+ try {
+ await page2.goto(`${BASE}/`, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ await page2.waitForTimeout(4000);
+
+ const canvasVisible = await page2.evaluate(() => {
+ const c = document.getElementById('showroom-canvas');
+ return c && c.offsetWidth > 0;
+ });
+ const webglOk = await page2.evaluate(() => {
+ try { const c = document.createElement('canvas'); const g = c.getContext('webgl'); return !!g; } catch(e) { return false; }
+ });
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'page-loaded.png') });
+ bbR('Main page load + WebGL', canvasVisible, webglOk, `canvas=${canvasVisible} webgl=${webglOk}`);
+
+ // Version picker
+ const pickerEl = await page2.$('#version-picker');
+ bbR('Version picker', !!pickerEl, !!pickerEl);
+
+ // Click V1 chip
+ const v1 = await page2.$('[data-v="V1"]');
+ if (v1) {
+ await v1.click();
+ await page2.waitForTimeout(1500);
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'v1-after.png') });
+ bbR('V1 chip click', true, true);
+ } else {
+ bbR('V1 chip click', false, false, 'not found');
+ }
+
+ // Click V6 overlay chip
+ const v6 = await page2.$('[data-v="V6"]');
+ if (v6) {
+ await v6.click();
+ await page2.waitForTimeout(2000);
+ const overlayVisible = await page2.evaluate(() => {
+ const h = document.getElementById('mechanic-overlay');
+ return h ? h.offsetWidth > 0 : false;
+ });
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'v6-overlay.png') });
+ bbR('V6 overlay chip', true, overlayVisible, `overlay=${overlayVisible}`);
+ } else {
+ bbR('V6 overlay chip', false, false, 'not found');
+ }
+
+ // Guided nav
+ const gNext2 = await page2.$('#g-next');
+ if (gNext2) {
+ await gNext2.click();
+ await page2.waitForTimeout(1200);
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'guided-next.png') });
+ bbR('Guided Next', true, true);
+ } else {
+ bbR('Guided Next', false, false, 'not found');
+ }
+
+ // Numbers overlay
+ // First go to V1
+ const v1b = await page2.$('[data-v="V1"]');
+ if (v1b) { await v1b.click(); await page2.waitForTimeout(1500); }
+ const vrBtn2 = await page2.$('#vr-overlay-btn');
+ if (vrBtn2) {
+ await vrBtn2.click();
+ await page2.waitForTimeout(1200);
+ const pinCount = await page2.evaluate(() => document.querySelectorAll('.el-pin').length);
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'numbers-overlay.png') });
+ bbR('Numbers overlay', true, pinCount > 0, `pinCount=${pinCount}`);
+ } else {
+ bbR('Numbers overlay', false, false, 'vr-overlay-btn not found');
+ }
+
+ // Slider test
+ const densityRange2 = await page2.$('#density-range');
+ if (densityRange2) {
+ await densityRange2.fill('12');
+ await densityRange2.dispatchEvent('input');
+ await page2.waitForTimeout(800);
+ bbR('Density slider', true, true);
+ } else {
+ bbR('Density slider', false, false, 'not found');
+ }
+
+ await page2.screenshot({ path: path.join(SHOTS, 'browserbase', 'final-state.png') });
+
+ } catch (e) {
+ log('browserbase', `Error during test: ${e.message}`);
+ results.push({ component: 'Browserbase run error', engine: 'browserbase', rendered: false, worked: false,
+ errors: 0, notes: `RUNTIME ERROR: ${e.message}` });
+ }
+
+ try { await browser2.close(); } catch (e) {}
+
+ // Replay URL
+ const replayUrl = `https://browserbase.com/sessions/${sessionId}`;
+ log('browserbase', `Session replay: ${replayUrl}`);
+ return replayUrl;
+}
+
+// ── Report generator ──────────────────────────────────────────────────────────
+function generateReport(results, bbReplay, engineErrors) {
+ const engines = ['chrome', 'safari', 'browserbase'];
+ const components = [...new Set(results.map(r => r.component))];
+
+ // Find broken items per engine
+ const broken = {};
+ for (const eng of engines) {
+ broken[eng] = results.filter(r => r.engine === eng && (!r.rendered || !r.worked)).map(r => r.component);
+ }
+
+ // Chrome vs Safari mismatches
+ const mismatches = [];
+ for (const comp of components) {
+ const cr = results.find(r => r.engine === 'chrome' && r.component === comp);
+ const sa = results.find(r => r.engine === 'safari' && r.component === comp);
+ if (cr && sa) {
+ if (cr.rendered !== sa.rendered || cr.worked !== sa.worked) {
+ mismatches.push({ comp, chrome: cr, safari: sa });
+ }
+ }
+ }
+
+ let md = `# CTA Report — Quadrille Showroom\n`;
+ md += `**Date:** ${new Date().toISOString()}\n`;
+ md += `**Target:** ${BASE}\n`;
+ md += `**Project:** /Users/stevestudio2/Projects/quadrille-showroom\n\n`;
+
+ // BROKEN section at top
+ const anyBroken = engines.some(e => broken[e].length > 0);
+ if (anyBroken) {
+ for (const eng of engines) {
+ if (broken[eng].length > 0) {
+ md += `## BROKEN IN ${eng.toUpperCase()}\n`;
+ for (const c of broken[eng]) {
+ const r = results.find(x => x.engine === eng && x.component === c);
+ md += `- **${c}** — rendered=${r.rendered} worked=${r.worked} errors=${r.errors}${r.notes ? ` | ${r.notes}` : ''}\n`;
+ }
+ md += '\n';
+ }
+ }
+ } else {
+ md += `## NO BROKEN COMPONENTS\nAll components rendered and worked across all tested engines.\n\n`;
+ }
+
+ // Engine-level errors
+ if (Object.keys(engineErrors).length) {
+ md += `## Engine-Level Notes\n`;
+ for (const [eng, note] of Object.entries(engineErrors)) {
+ md += `- **${eng}**: ${note}\n`;
+ }
+ md += '\n';
+ }
+
+ // Component × Engine table
+ md += `## Component × Engine Matrix\n\n`;
+ md += `| Component | Chrome render | Chrome work | Chrome errs | Safari render | Safari work | Safari errs | BB render | BB work | BB errs |\n`;
+ md += `|-----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n`;
+
+ for (const comp of components) {
+ const vals = {};
+ for (const eng of engines) {
+ const r = results.find(x => x.engine === eng && x.component === comp);
+ vals[eng] = r || { rendered: null, worked: null, errors: '—' };
+ }
+ const cell = (r, key) => r[key] === null ? '—' : (r[key] ? '✓' : '✗');
+ md += `| ${comp} | ${cell(vals.chrome,'rendered')} | ${cell(vals.chrome,'worked')} | ${vals.chrome.errors} | ${cell(vals.safari,'rendered')} | ${cell(vals.safari,'worked')} | ${vals.safari.errors} | ${cell(vals.browserbase,'rendered')} | ${cell(vals.browserbase,'worked')} | ${vals.browserbase.errors} |\n`;
+ }
+
+ // Chrome vs Safari mismatches
+ md += `\n## Chrome-vs-Safari Mismatches\n\n`;
+ if (mismatches.length === 0) {
+ md += 'No mismatches detected between Chrome and Safari.\n';
+ } else {
+ for (const m of mismatches) {
+ md += `### ${m.comp}\n`;
+ md += `- Chrome: rendered=${m.chrome.rendered} worked=${m.chrome.worked} errors=${m.chrome.errors} | ${m.chrome.notes}\n`;
+ md += `- Safari: rendered=${m.safari.rendered} worked=${m.safari.worked} errors=${m.safari.errors} | ${m.safari.notes}\n\n`;
+ }
+ }
+
+ // Browserbase info
+ md += `\n## Browserbase Session\n`;
+ if (bbReplay) {
+ md += `- Session replay URL: ${bbReplay}\n`;
+ md += `- Cost estimate: ~$0.01–0.05 (one session)\n`;
+ } else {
+ md += `- Browserbase session was skipped or failed — see notes above.\n`;
+ }
+
+ // Recordings
+ md += `\n## Recordings & Screenshots\n`;
+ md += `- Chrome recording: /Users/stevestudio2/Projects/quadrille-showroom/cta/rec/chrome.mp4 (or .webm)\n`;
+ md += `- Safari recording: /Users/stevestudio2/Projects/quadrille-showroom/cta/rec/safari.mp4 (or .webm)\n`;
+ md += `- Screenshots: /Users/stevestudio2/Projects/quadrille-showroom/cta/shots/{chrome,safari,browserbase}/\n\n`;
+
+ // Verdict
+ const chromePass = results.filter(r => r.engine === 'chrome').every(r => r.rendered && r.worked);
+ const safariPass = results.filter(r => r.engine === 'safari').every(r => r.rendered && r.worked);
+ const chromeErrors = results.filter(r => r.engine === 'chrome').reduce((s,r) => s + r.errors, 0);
+ const safariErrors = results.filter(r => r.engine === 'safari').reduce((s,r) => s + r.errors, 0);
+
+ md += `## Verdict\n`;
+ if (chromePass && safariPass && chromeErrors === 0 && safariErrors === 0) {
+ md += `**PASS** — All components render and work in Chrome and Safari with 0 console errors.\n`;
+ } else {
+ md += `**FAIL** —\n`;
+ if (!chromePass) md += `- Chrome: ${broken.chrome.join(', ')}\n`;
+ if (!safariPass) md += `- Safari: ${broken.safari.join(', ')}\n`;
+ if (chromeErrors > 0) md += `- Chrome console errors: ${chromeErrors}\n`;
+ if (safariErrors > 0) md += `- Safari console errors: ${safariErrors}\n`;
+ }
+
+ return md;
+}
+
+// ── Entry point ───────────────────────────────────────────────────────────────
+async function main() {
+ const results = [];
+ const engineErrors = {};
+ let bbReplay = null;
+
+ // Chrome
+ {
+ log('chrome', 'Launching Chromium...');
+ let browser;
+ try {
+ browser = await chromium.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-webgl', '--use-gl=swiftshader',
+ '--ignore-gpu-blocklist', '--disable-gpu-sandbox']
+ });
+ await runEngine('chrome', browser, results, null);
+ } catch (e) {
+ engineErrors.chrome = `LAUNCH/RUN ERROR: ${e.message}`;
+ log('chrome', `ERROR: ${e.message}`);
+ } finally {
+ if (browser) try { await browser.close(); } catch (e) {}
+ }
+ }
+
+ // Safari/WebKit
+ {
+ log('safari', 'Launching WebKit...');
+ let browser;
+ try {
+ browser = await webkit.launch({ headless: true });
+ await runEngine('safari', browser, results, null);
+ } catch (e) {
+ engineErrors.safari = `LAUNCH/RUN ERROR: ${e.message}`;
+ log('safari', `ERROR: ${e.message}`);
+ } finally {
+ if (browser) try { await browser.close(); } catch (e) {}
+ }
+ }
+
+ // Browserbase (cloud) — estimated cost: ~$0.01–0.05 for one session
+ log('browserbase', 'Starting Browserbase session (est. cost: ~$0.01–0.05)...');
+ try {
+ bbReplay = await runBrowserbase(results);
+ } catch (e) {
+ engineErrors.browserbase = `ERROR: ${e.message}`;
+ log('browserbase', `ERROR: ${e.message}`);
+ }
+
+ // Convert webm → mp4 via ffmpeg
+ log('ffmpeg', 'Converting recordings...');
+ for (const eng of ['chrome', 'safari']) {
+ const recDir = path.join(RECS, eng);
+ if (!fs.existsSync(recDir)) continue;
+ const webms = fs.readdirSync(recDir).filter(f => f.endsWith('.webm'));
+ for (const webm of webms) {
+ const src = path.join(recDir, webm);
+ const dst = path.join(RECS, `${eng}.mp4`);
+ const { execSync } = require('child_process');
+ try {
+ execSync(`ffmpeg -y -i "${src}" -c:v libx264 -preset fast -crf 23 "${dst}" 2>/dev/null`);
+ log('ffmpeg', `${eng}: ${webm} → ${eng}.mp4`);
+ } catch (e) {
+ log('ffmpeg', `ffmpeg missing or failed for ${eng}, leaving .webm: ${e.message}`);
+ engineErrors[`${eng}-recording`] = `ffmpeg failed — .webm available at ${src}`;
+ // Copy webm as named file for reference
+ try { fs.copyFileSync(src, path.join(RECS, `${eng}.webm`)); } catch (e2) {}
+ }
+ }
+ }
+
+ // Generate report
+ log('report', 'Generating CTA-REPORT.md...');
+ const report = generateReport(results, bbReplay, engineErrors);
+ const reportPath = path.join(OUTDIR, 'CTA-REPORT.md');
+ fs.writeFileSync(reportPath, report);
+ log('report', `Written to ${reportPath}`);
+
+ // Summary to stdout
+ console.log('\n=== CTA SUMMARY ===');
+ console.log(`Components tested: ${[...new Set(results.map(r => r.component))].length}`);
+ console.log(`Chrome results: ${results.filter(r=>r.engine==='chrome').length} components`);
+ console.log(`Safari results: ${results.filter(r=>r.engine==='safari').length} components`);
+ console.log(`Browserbase results: ${results.filter(r=>r.engine==='browserbase').length} components`);
+ if (bbReplay) console.log(`Browserbase replay: ${bbReplay}`);
+ console.log(`Report: ${reportPath}`);
+
+ // Print broken summary
+ for (const eng of ['chrome','safari','browserbase']) {
+ const broken = results.filter(r => r.engine === eng && (!r.rendered || !r.worked));
+ if (broken.length) {
+ console.log(`\nBROKEN IN ${eng.toUpperCase()}:`);
+ broken.forEach(r => console.log(` - ${r.component}: rendered=${r.rendered} worked=${r.worked} | ${r.notes}`));
+ }
+ }
+
+ console.log('\n=== DONE ===');
+}
+
+main().catch(e => { console.error('FATAL:', e); process.exit(1); });
diff --git a/cta/rec/chrome.mp4 b/cta/rec/chrome.mp4
new file mode 100644
index 0000000..17e5e5f
Binary files /dev/null and b/cta/rec/chrome.mp4 differ
diff --git a/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm b/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm
new file mode 100644
index 0000000..5149670
Binary files /dev/null and b/cta/rec/chrome/page@005780cf82e25d7a610952578a1e8b01.webm differ
diff --git a/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm b/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm
new file mode 100644
index 0000000..5c6d385
Binary files /dev/null and b/cta/rec/chrome/page@0d47eaf4e739239db687c75f5ef86e35.webm differ
diff --git a/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm b/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm
new file mode 100644
index 0000000..230a901
Binary files /dev/null and b/cta/rec/chrome/page@276ba27d702558593b3f7df9491e1e7f.webm differ
diff --git a/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm b/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm
new file mode 100644
index 0000000..dde7f43
Binary files /dev/null and b/cta/rec/chrome/page@36c329fa87ea2500c594894cbece88d7.webm differ
diff --git a/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm b/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm
new file mode 100644
index 0000000..8afe169
Binary files /dev/null and b/cta/rec/chrome/page@52919fd1750ab7668ebd008ad424761a.webm differ
diff --git a/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm b/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm
new file mode 100644
index 0000000..17228f0
Binary files /dev/null and b/cta/rec/chrome/page@606eb2d575ea0509a05fcbf0103af978.webm differ
diff --git a/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm b/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm
new file mode 100644
index 0000000..a201f9f
Binary files /dev/null and b/cta/rec/chrome/page@8d3b7b31973fa296d9c51ef2f8ae6b42.webm differ
diff --git a/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm b/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm
new file mode 100644
index 0000000..a61fd2e
Binary files /dev/null and b/cta/rec/safari/page@b60d9b1ed1b787681487db1c6e068fd1.webm differ
diff --git a/cta/shots/chrome/all-designs-after.png b/cta/shots/chrome/all-designs-after.png
new file mode 100644
index 0000000..c0668e6
Binary files /dev/null and b/cta/shots/chrome/all-designs-after.png differ
diff --git a/cta/shots/chrome/all-designs-before.png b/cta/shots/chrome/all-designs-before.png
new file mode 100644
index 0000000..f0dabfa
Binary files /dev/null and b/cta/shots/chrome/all-designs-before.png differ
diff --git a/cta/shots/chrome/auto-tour-after.png b/cta/shots/chrome/auto-tour-after.png
new file mode 100644
index 0000000..6b5b688
Binary files /dev/null and b/cta/shots/chrome/auto-tour-after.png differ
diff --git a/cta/shots/chrome/auto-tour-before.png b/cta/shots/chrome/auto-tour-before.png
new file mode 100644
index 0000000..5bbdb3c
Binary files /dev/null and b/cta/shots/chrome/auto-tour-before.png differ
diff --git a/cta/shots/chrome/final-state.png b/cta/shots/chrome/final-state.png
new file mode 100644
index 0000000..ae19791
Binary files /dev/null and b/cta/shots/chrome/final-state.png differ
diff --git a/cta/shots/chrome/guided-next-after.png b/cta/shots/chrome/guided-next-after.png
new file mode 100644
index 0000000..fd4258c
Binary files /dev/null and b/cta/shots/chrome/guided-next-after.png differ
diff --git a/cta/shots/chrome/guided-next-before.png b/cta/shots/chrome/guided-next-before.png
new file mode 100644
index 0000000..fc09bce
Binary files /dev/null and b/cta/shots/chrome/guided-next-before.png differ
diff --git a/cta/shots/chrome/guided-prev-after.png b/cta/shots/chrome/guided-prev-after.png
new file mode 100644
index 0000000..5bbdb3c
Binary files /dev/null and b/cta/shots/chrome/guided-prev-after.png differ
diff --git a/cta/shots/chrome/numbers-overlay-before.png b/cta/shots/chrome/numbers-overlay-before.png
new file mode 100644
index 0000000..810ad7f
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-before.png differ
diff --git a/cta/shots/chrome/numbers-overlay-off.png b/cta/shots/chrome/numbers-overlay-off.png
new file mode 100644
index 0000000..fc09bce
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-off.png differ
diff --git a/cta/shots/chrome/numbers-overlay-on.png b/cta/shots/chrome/numbers-overlay-on.png
new file mode 100644
index 0000000..ae2d2dd
Binary files /dev/null and b/cta/shots/chrome/numbers-overlay-on.png differ
diff --git a/cta/shots/chrome/page-loaded.png b/cta/shots/chrome/page-loaded.png
new file mode 100644
index 0000000..5d8ba75
Binary files /dev/null and b/cta/shots/chrome/page-loaded.png differ
diff --git a/cta/shots/chrome/pin-clicked-tray.png b/cta/shots/chrome/pin-clicked-tray.png
new file mode 100644
index 0000000..ea489f5
Binary files /dev/null and b/cta/shots/chrome/pin-clicked-tray.png differ
diff --git a/cta/shots/chrome/sample-table-after.png b/cta/shots/chrome/sample-table-after.png
new file mode 100644
index 0000000..e009872
Binary files /dev/null and b/cta/shots/chrome/sample-table-after.png differ
diff --git a/cta/shots/chrome/sample-table-before.png b/cta/shots/chrome/sample-table-before.png
new file mode 100644
index 0000000..e009872
Binary files /dev/null and b/cta/shots/chrome/sample-table-before.png differ
diff --git a/cta/shots/chrome/slider-angle-after.png b/cta/shots/chrome/slider-angle-after.png
new file mode 100644
index 0000000..36c897e
Binary files /dev/null and b/cta/shots/chrome/slider-angle-after.png differ
diff --git a/cta/shots/chrome/slider-angle-before.png b/cta/shots/chrome/slider-angle-before.png
new file mode 100644
index 0000000..088f792
Binary files /dev/null and b/cta/shots/chrome/slider-angle-before.png differ
diff --git a/cta/shots/chrome/slider-density-after.png b/cta/shots/chrome/slider-density-after.png
new file mode 100644
index 0000000..0d65478
Binary files /dev/null and b/cta/shots/chrome/slider-density-after.png differ
diff --git a/cta/shots/chrome/slider-density-before.png b/cta/shots/chrome/slider-density-before.png
new file mode 100644
index 0000000..4d58cef
Binary files /dev/null and b/cta/shots/chrome/slider-density-before.png differ
diff --git a/cta/shots/chrome/slider-reveal-after.png b/cta/shots/chrome/slider-reveal-after.png
new file mode 100644
index 0000000..088f792
Binary files /dev/null and b/cta/shots/chrome/slider-reveal-after.png differ
diff --git a/cta/shots/chrome/slider-reveal-before.png b/cta/shots/chrome/slider-reveal-before.png
new file mode 100644
index 0000000..0d65478
Binary files /dev/null and b/cta/shots/chrome/slider-reveal-before.png differ
diff --git a/cta/shots/chrome/sort-after.png b/cta/shots/chrome/sort-after.png
new file mode 100644
index 0000000..f57e151
Binary files /dev/null and b/cta/shots/chrome/sort-after.png differ
diff --git a/cta/shots/chrome/sort-before.png b/cta/shots/chrome/sort-before.png
new file mode 100644
index 0000000..159cbda
Binary files /dev/null and b/cta/shots/chrome/sort-before.png differ
diff --git a/cta/shots/chrome/v10-concierge-after.png b/cta/shots/chrome/v10-concierge-after.png
new file mode 100644
index 0000000..303ef1e
Binary files /dev/null and b/cta/shots/chrome/v10-concierge-after.png differ
diff --git a/cta/shots/chrome/v10-concierge-before.png b/cta/shots/chrome/v10-concierge-before.png
new file mode 100644
index 0000000..e7bdb69
Binary files /dev/null and b/cta/shots/chrome/v10-concierge-before.png differ
diff --git a/cta/shots/chrome/v6-colorriver-after.png b/cta/shots/chrome/v6-colorriver-after.png
new file mode 100644
index 0000000..70b0f1d
Binary files /dev/null and b/cta/shots/chrome/v6-colorriver-after.png differ
diff --git a/cta/shots/chrome/v6-colorriver-before.png b/cta/shots/chrome/v6-colorriver-before.png
new file mode 100644
index 0000000..c056074
Binary files /dev/null and b/cta/shots/chrome/v6-colorriver-before.png differ
diff --git a/cta/shots/chrome/v7-moodboard-after.png b/cta/shots/chrome/v7-moodboard-after.png
new file mode 100644
index 0000000..275ab5a
Binary files /dev/null and b/cta/shots/chrome/v7-moodboard-after.png differ
diff --git a/cta/shots/chrome/v7-moodboard-before.png b/cta/shots/chrome/v7-moodboard-before.png
new file mode 100644
index 0000000..ba75fa8
Binary files /dev/null and b/cta/shots/chrome/v7-moodboard-before.png differ
diff --git a/cta/shots/chrome/v8-swipe-after.png b/cta/shots/chrome/v8-swipe-after.png
new file mode 100644
index 0000000..8cea1d2
Binary files /dev/null and b/cta/shots/chrome/v8-swipe-after.png differ
diff --git a/cta/shots/chrome/v8-swipe-before.png b/cta/shots/chrome/v8-swipe-before.png
new file mode 100644
index 0000000..8cea1d2
Binary files /dev/null and b/cta/shots/chrome/v8-swipe-before.png differ
diff --git a/cta/shots/chrome/v9-walkin-after.png b/cta/shots/chrome/v9-walkin-after.png
new file mode 100644
index 0000000..64ddb1c
Binary files /dev/null and b/cta/shots/chrome/v9-walkin-after.png differ
diff --git a/cta/shots/chrome/v9-walkin-before.png b/cta/shots/chrome/v9-walkin-before.png
new file mode 100644
index 0000000..ec72102
Binary files /dev/null and b/cta/shots/chrome/v9-walkin-before.png differ
diff --git a/cta/shots/chrome/version-picker-before.png b/cta/shots/chrome/version-picker-before.png
new file mode 100644
index 0000000..5d8ba75
Binary files /dev/null and b/cta/shots/chrome/version-picker-before.png differ
diff --git a/cta/shots/chrome/window-next-before.png b/cta/shots/chrome/window-next-before.png
new file mode 100644
index 0000000..159cbda
Binary files /dev/null and b/cta/shots/chrome/window-next-before.png differ
diff --git a/cta/shots/chrome/wing-click-before.png b/cta/shots/chrome/wing-click-before.png
new file mode 100644
index 0000000..999aa7b
Binary files /dev/null and b/cta/shots/chrome/wing-click-before.png differ
diff --git a/cta/shots/chrome/wing-detail-after-click.png b/cta/shots/chrome/wing-detail-after-click.png
new file mode 100644
index 0000000..a3f4531
Binary files /dev/null and b/cta/shots/chrome/wing-detail-after-click.png differ
diff --git a/cta/shots/safari/numbers-overlay-before.png b/cta/shots/safari/numbers-overlay-before.png
new file mode 100644
index 0000000..c059261
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-before.png differ
diff --git a/cta/shots/safari/numbers-overlay-off.png b/cta/shots/safari/numbers-overlay-off.png
new file mode 100644
index 0000000..72862c2
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-off.png differ
diff --git a/cta/shots/safari/numbers-overlay-on.png b/cta/shots/safari/numbers-overlay-on.png
new file mode 100644
index 0000000..0a2bef1
Binary files /dev/null and b/cta/shots/safari/numbers-overlay-on.png differ
diff --git a/cta/shots/safari/page-loaded.png b/cta/shots/safari/page-loaded.png
new file mode 100644
index 0000000..ebd7db8
Binary files /dev/null and b/cta/shots/safari/page-loaded.png differ
diff --git a/cta/shots/safari/pin-clicked-tray.png b/cta/shots/safari/pin-clicked-tray.png
new file mode 100644
index 0000000..2996411
Binary files /dev/null and b/cta/shots/safari/pin-clicked-tray.png differ
diff --git a/cta/shots/safari/version-picker-before.png b/cta/shots/safari/version-picker-before.png
new file mode 100644
index 0000000..c059261
Binary files /dev/null and b/cta/shots/safari/version-picker-before.png differ
← 49978bc auto-save: 2026-06-28T10:44:13 (5 files) — data/thumb-cache/
·
back to Quadrille Showroom
·
Slice 1 (A+B+C): 14x14 room, fixed-centre yaw-spin camera (c 1fbedd2 →