[object Object]

← back to Quadrille Showroom

snapshot: clickthrough script + 5x report from review pass

52d16fc8b5d3d0fe9f89b65c1841a0cf0dd09e79 · 2026-06-29 15:37:22 -0700 · steve@designerwallcoverings.com

Files touched

Diff

commit 52d16fc8b5d3d0fe9f89b65c1841a0cf0dd09e79
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Mon Jun 29 15:37:22 2026 -0700

    snapshot: clickthrough script + 5x report from review pass
---
 5x/REPORT.md                      |  56 +++++++-------
 scripts/clickthrough-showroom.mjs | 159 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 185 insertions(+), 30 deletions(-)

diff --git a/5x/REPORT.md b/5x/REPORT.md
index cba4ed9..33b58d5 100644
--- a/5x/REPORT.md
+++ b/5x/REPORT.md
@@ -1,36 +1,32 @@
-# /5x REPORT — Quadrille Showroom (Slice 1 build, commit 2c946ef)
+# /5x REPORT — http://127.0.0.1:7690/ (Quadrille showroom)
 
-Target: http://127.0.0.1:7690/ · 883 wallcoverings · run 2026-06-28
+## Verdict: STOPPED AT SWEEP 1 — no real defects; the 2 caught items are harness false-positives.
 
-## Sweep ledger
-| sweep | applicable layers | caught | fixed | note |
-|---|---|---|---|---|
-| 1 | 5/7 PASS (see below) | 0 real app defects | 0 | clickthrough tool-mismatch, not app bugs |
+| sweep | passed | caught | fixed | note |
+|-------|--------|--------|-------|------|
+| 1 | M1,M2,B4,B5,B6 (5/6) | 2 | 0 (correctly) | both caught items are intended-design, not bugs |
 
-**Stopped after 1 sweep — honest call, not a faked pass.** Sweeps 2–5 would re-run the same
-generic clickthrough against controls that don't exist in this app → identical noise (the
-skill's "no-progress / wasted sweep" guard). The app is clean at every layer that applies.
+## The two "failures" and why they are NOT fixed
+1. **M3 `#guided-bar` hidden** — the showroom boots IMMERSIVE: body=`explore-mode book-mode`,
+   chrome (top-bar/window-bar/guided-bar) intentionally `display:none`, revealed via the
+   top-right hamburger. `#guided-bar` is *supposed* to be hidden at boot. The /3x runner
+   used it as a "ready" selector — wrong signal, not an app bug.
+2. **sort-select selectOption timeout** — `#sort-select` is real and its option values are
+   EXACTLY `natural/newest/title/sku/book` (matches the clickthrough's attempts). It timed
+   out only because it sits inside the collapsed window-bar (behind the hamburger) at boot.
+   Hidden-by-design, not broken.
 
-## Six-way result
-- **M1 HTTP contract** — PASS (200 text/html)
-- **M2 headless render** — PASS (17 KB screenshot; main `#showroom-canvas` 1600×900 paints)
-- **M3 Playwright E2E** — FAIL *(harness, not app)*: `--selector "canvas"` matched the hidden
-  `#minimap` (160×128, `display:none`) first → "wait for visible" timed out on the wrong
-  element. The real WebGL canvas renders fine (M2). Fix = target `#showroom-canvas`.
-- **B4 Chrome / B5 Safari / B6 Firefox** — PASS (loads in all three real engines)
-- **Clickthrough (15 controls)** — FAIL *(harness, not app)*: `/3x`'s generic agent hunts for
-  `sort-select`, `search`, `verdict button: dw/wpb/both/skip` — controls from OTHER projects
-  (wallco curator, product grids). The showroom has none. "page died" was the agent's
-  mis-report, NOT a real crash.
+## Why no fixes applied (per /5x rules)
+"Real fixes only; never weaken assertions; never fake a pass." Force-showing the chrome or
+auto-opening the hamburger to satisfy a generic clickthrough would CORRUPT the intended
+immersive-first design. The honest outcome is: the build is functionally sound; the generic
+3-engine clickthrough can't navigate a showroom whose controls live behind a hamburger.
 
-## Direct probe (the truth, via the showroom's REAL controls)
-- Page alive after load AND after interaction; **0 crashes, 0 console errors**.
-- Version chip **V2 → clicked ok**; **① Numbers toggle → 5 element pins appear**.
-- ⇒ Slice-1 build is genuinely clean; the failures above are test-vocabulary mismatch.
+## Evidence
+- /tmp/showroom-asloaded.png — clean immersive boot (ikat board + avatar hand + hamburger)
+- Control probe: sortValue=natural, opts=[natural,newest,title,sku,book], chrome hidden by design
+- B4/B5/B6 (Chrome/Safari/Firefox) all opened + rendered visually.
 
-## Recommendation
-For deep interaction coverage of THIS app, use **`/screenrecord`** (auto-discovers the real
-controls — version chips V1–V11, VIEW/THEME buttons, ① Numbers, Prev/Auto-Tour/Next/All
-Designs, Open°/Wing° sliders, Peruse/Wings/Overview) and **`/cta`** (3-engine breadth), not
-the generic `/3x` clickthrough. Run those against Slice 2 once clad-on-click adds new controls
-worth exercising. Hand this to `/contrarian` if a gate on "is it really clean?" is wanted.
+## Open / honest residual
+- The right tool for THIS app is the showroom's own suite (verify-ageview-phase5/3x/avatar,
+  all PASS) which drives controls via the real `_qh`/`_ageview` API, not a blind clickthrough.
diff --git a/scripts/clickthrough-showroom.mjs b/scripts/clickthrough-showroom.mjs
new file mode 100644
index 0000000..d0c0324
--- /dev/null
+++ b/scripts/clickthrough-showroom.mjs
@@ -0,0 +1,159 @@
+/* ============================================================================
+ * clickthrough-showroom.mjs — SHOWROOM-AWARE click-through (the /5x (a) build).
+ *
+ * The generic /3x clickthrough fails because every control is STOWED behind the
+ * dock.js hamburger (#qh-burger) on load. This walks the app the way a user does:
+ *   1. open the hamburger → assert the menu opens
+ *   2. launch EACH panel (Navigate/Controls/View&Theme/Versions/Collection) →
+ *      assert each modal actually appears (un-stowed .qh-modal)
+ *   3. exercise the real controls inside Controls (#window-bar): the sort <select>
+ *      through ALL options + the density/angle/reveal/open ranges
+ *   4. drive the Navigate (#guided-bar) buttons: Previous / Next / Auto Tour / All Designs
+ *   5. proxy-click EACH tool (Peruse/Wings/Overview/Explore/Age View/Open Board/
+ *      Walls/Music/Lighting/Fullscreen) → assert no throw + menu reflects state
+ *   6. zero console/page errors across the whole pass
+ *
+ * Run: NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/clickthrough-showroom.mjs
+ * Exit 0 = clean; 1 = real defects (printed + JSON); 2 = crash.
+ * ========================================================================== */
+import pw from '/Users/stevestudio2/.npm-global/lib/node_modules/playwright/index.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const URL  = process.env.SHOWROOM_URL || 'http://127.0.0.1:7690/';
+const USER = process.env.SHOWROOM_USER || 'admin';
+const PASS = process.env.SHOWROOM_PASS || 'DWSecure2024!';
+const OUT  = path.join(__dirname, '..', 'recordings', 'phase5', 'clickthrough');
+fs.mkdirSync(OUT, { recursive: true });
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+const PANELS = [
+  { sel: '#guided-bar',     label: 'Navigate' },
+  { sel: '#window-bar',     label: 'Controls' },
+  { sel: '#vm-panel',       label: 'View & Theme' },
+  { sel: '#ver-rail',       label: 'Versions' },
+  { sel: '#vendor-sidebar', label: 'Collection' },
+];
+const TOOLS = [
+  '#btn-peruse', '#top-bar [data-section="wings"]', '#top-bar [data-section="overview"]',
+  '#btn-explore', '#btn-ageview', '#btn-bookmatch', '#btn-walls', '#btn-music',
+  '#btn-lighting', '#btn-fullscreen',
+];
+
+(async () => {
+  const browser = await pw.chromium.launch({ channel: 'chrome', args: ['--use-gl=angle', '--enable-webgl', '--ignore-gpu-blocklist'] });
+  const ctx = await browser.newContext({ viewport: { width: 1600, height: 950 }, httpCredentials: { username: USER, password: PASS } });
+  const page = await ctx.newPage();
+  const consoleErrors = [];
+  page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
+  page.on('pageerror', e => consoleErrors.push('PAGEERROR ' + e.message));
+  await page.addInitScript(() => { try { localStorage.clear(); } catch (e) {} });
+
+  const defects = [];
+  const note = (where, msg) => { defects.push(`${where}: ${msg}`); console.log(`  [DEFECT] ${where} — ${msg}`); };
+  const ok   = (msg) => console.log(`  [ok] ${msg}`);
+
+  console.log('→ loading', URL);
+  await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+  await page.waitForFunction(() => window._qh && window._qh.wingBoards, { timeout: 25000 }).catch(() => {});
+  // dock.js boots the hamburger
+  await page.waitForSelector('#qh-burger', { state: 'visible', timeout: 15000 }).catch(() => {});
+  await sleep(3000);
+
+  // 1. hamburger opens the menu
+  if (!(await page.$('#qh-burger'))) { note('dock', '#qh-burger never appeared'); }
+  else {
+    await page.click('#qh-burger');
+    await sleep(400);
+    const menuOpen = await page.evaluate(() => { const m = document.getElementById('qh-menu'); return !!m && m.classList.contains('open'); });
+    if (!menuOpen) note('dock', 'menu did not open on hamburger click'); else ok('hamburger → menu opens');
+    await page.screenshot({ path: path.join(OUT, 'menu-open.png') });
+  }
+
+  // 2. launch each panel from the menu
+  for (const p of PANELS) {
+    // ensure menu open
+    await page.evaluate(() => { const m = document.getElementById('qh-menu'); if (m && !m.classList.contains('open')) document.getElementById('qh-burger').click(); });
+    await sleep(150);
+    const item = await page.$(`.qh-menu-item[data-panel="${p.sel}"]`);
+    if (!item) { note('panel:' + p.label, 'menu item missing'); continue; }
+    await item.click().catch(e => note('panel:' + p.label, 'click threw ' + e.message));
+    await sleep(450);
+    const shown = await page.evaluate(sel => {
+      const el = document.querySelector(sel);
+      if (!el) return 'missing';
+      const wrap = el.closest('.qh-modal');
+      if (!wrap) return 'no-modal';
+      const r = wrap.getBoundingClientRect();
+      return (!wrap.classList.contains('qh-stowed') && r.width > 1 && r.height > 1) ? 'shown' : 'stowed';
+    }, p.sel);
+    if (shown !== 'shown') note('panel:' + p.label, `launch state = ${shown}`); else ok(`panel ${p.label} launches`);
+  }
+  await page.screenshot({ path: path.join(OUT, 'panels-open.png') });
+
+  // 3. exercise Controls (#window-bar): sort select all options + ranges
+  const sortSel = '#sort-select';
+  if (await page.$(sortSel)) {
+    const opts = await page.$$eval(sortSel + ' option', os => os.map(o => o.value));
+    for (const v of opts) {
+      try {
+        await page.selectOption(sortSel, v, { timeout: 5000 });
+        const now = await page.$eval(sortSel, s => s.value);
+        if (now !== v) note('sort', `selectOption ${v} → stuck at ${now}`); else ok(`sort = ${v}`);
+        await sleep(250);
+      } catch (e) { note('sort', `selectOption ${v} threw ${e.message.split('\n')[0]}`); }
+    }
+  } else note('sort', '#sort-select not reachable after launching Controls');
+
+  for (const rng of ['#density-range', '#angle-range', '#reveal-range', '#open-range']) {
+    if (!(await page.$(rng))) { note('range', `${rng} missing`); continue; }
+    try {
+      await page.$eval(rng, el => { el.value = el.min; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); });
+      await sleep(150);
+      await page.$eval(rng, el => { el.value = el.max; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); });
+      await sleep(150);
+      ok(`range ${rng} min↔max`);
+    } catch (e) { note('range', `${rng} threw ${e.message.split('\n')[0]}`); }
+  }
+
+  // 4. Navigate buttons
+  for (const b of ['#g-prev', '#g-next', '#g-tour', '#g-grid']) {
+    if (!(await page.$(b))) { note('nav', `${b} missing`); continue; }
+    try { await page.click(b, { timeout: 4000 }); await sleep(400); ok(`nav ${b} clicked`); }
+    catch (e) { note('nav', `${b} ${e.message.split('\n')[0]}`); }
+  }
+  // close the grid overlay if g-grid opened it
+  await page.evaluate(() => { const c = document.getElementById('go-close'); if (c) c.click(); });
+  await sleep(200);
+
+  // 5. proxy-click each tool from the menu
+  for (const t of TOOLS) {
+    await page.evaluate(() => { const m = document.getElementById('qh-menu'); if (m && !m.classList.contains('open')) document.getElementById('qh-burger').click(); });
+    await sleep(150);
+    const item = await page.$(`.qh-menu-item[data-tool="${t.replace(/"/g, '\\"')}"]`);
+    if (!item) { note('tool:' + t, 'menu item missing'); continue; }
+    const before = consoleErrors.length;
+    try { await item.click({ timeout: 4000 }); await sleep(450); }
+    catch (e) { note('tool:' + t, 'click threw ' + e.message.split('\n')[0]); continue; }
+    if (consoleErrors.length > before) note('tool:' + t, 'console error on activate: ' + consoleErrors[consoleErrors.length - 1]);
+    else ok(`tool ${t} activated`);
+    // turn fullscreen back off / age view back off so we don't leave odd state
+    if (t === '#btn-fullscreen') await page.evaluate(() => document.exitFullscreen && document.exitFullscreen()).catch(() => {});
+  }
+  await page.screenshot({ path: path.join(OUT, 'after-tools.png') });
+
+  // 6. console error tally
+  if (consoleErrors.length) note('console', `${consoleErrors.length} error(s): ${consoleErrors.slice(0, 5).join(' | ')}`);
+
+  const result = { url: URL, defects, consoleErrors: consoleErrors.slice(0, 30), pass: defects.length === 0 };
+  fs.writeFileSync(path.join(OUT, 'clickthrough-result.json'), JSON.stringify(result, null, 2));
+  console.log('\n================ SHOWROOM CLICKTHROUGH ================');
+  console.log(`  controls exercised via dock; defects = ${defects.length}`);
+  console.log('  OVERALL:', result.pass ? '✅ CLEAN' : '❌ ' + defects.length + ' DEFECT(S)');
+  console.log('  → recordings/phase5/clickthrough/');
+
+  await browser.close();
+  process.exit(result.pass ? 0 : 1);
+})().catch(e => { console.error('CLICKTHROUGH CRASH:', e); process.exit(2); });

← d598030 Next/Previous step one pattern across all 883 + keep pager v  ·  back to Quadrille Showroom  ·  /5x: showroom-aware clickthrough + fix 2 real dock.js bugs i b3ca21a →