[object Object]

← back to Quadrille Showroom

/5x: showroom-aware clickthrough + fix 2 real dock.js bugs it caught

b3ca21a5bef110ac3736ccc8f2dde56774551b10 · 2026-06-29 15:42:47 -0700 · Steve

The generic /3x clickthrough can't open the dock hamburger, so it false-failed on
intentionally-stowed controls. Built scripts/clickthrough-showroom.mjs that drives the
app like a user (open hamburger → launch each panel → exercise sort/sliders/nav → proxy
every tool). It caught 2 GENUINE defects the blind clickthrough never could:

1. dock.js quote-in-selector: TOOLS entries '#top-bar [data-section="wings"]' / '...overview'
   were interpolated raw into data-tool="..." (inner quotes close the attr) and into
   reflectMenu's querySelector('[data-tool="..."]') (throws 'not a valid selector'). Wings +
   Overview were unreachable from the menu and reflectMenu threw 14× PAGEERROR. Fix: key tools
   by index (data-tool-idx), resolve TOOLS[idx].sel in JS.

2. dock.js launched panels showed EMPTY: launch() removed qh-stowed from the wrapper but not
   from the panel element, so every launched panel's CONTENT stayed display:none!important —
   only the header bar showed (sort select measured 0×0, which is why selectOption timed out).
   Fix: el.classList.remove('qh-stowed') on launch. Panels now render their real content
   (verified by screenshot: Versions list + Controls sort/sliders visible).

Sweep 4 clean: sort all 5 options, all ranges, all nav, all 10 tools, 0 console errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b3ca21a5bef110ac3736ccc8f2dde56774551b10
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jun 29 15:42:47 2026 -0700

    /5x: showroom-aware clickthrough + fix 2 real dock.js bugs it caught
    
    The generic /3x clickthrough can't open the dock hamburger, so it false-failed on
    intentionally-stowed controls. Built scripts/clickthrough-showroom.mjs that drives the
    app like a user (open hamburger → launch each panel → exercise sort/sliders/nav → proxy
    every tool). It caught 2 GENUINE defects the blind clickthrough never could:
    
    1. dock.js quote-in-selector: TOOLS entries '#top-bar [data-section="wings"]' / '...overview'
       were interpolated raw into data-tool="..." (inner quotes close the attr) and into
       reflectMenu's querySelector('[data-tool="..."]') (throws 'not a valid selector'). Wings +
       Overview were unreachable from the menu and reflectMenu threw 14× PAGEERROR. Fix: key tools
       by index (data-tool-idx), resolve TOOLS[idx].sel in JS.
    
    2. dock.js launched panels showed EMPTY: launch() removed qh-stowed from the wrapper but not
       from the panel element, so every launched panel's CONTENT stayed display:none!important —
       only the header bar showed (sort select measured 0×0, which is why selectOption timed out).
       Fix: el.classList.remove('qh-stowed') on launch. Panels now render their real content
       (verified by screenshot: Versions list + Controls sort/sliders visible).
    
    Sweep 4 clean: sort all 5 options, all ranges, all nav, all 10 tools, 0 console errors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/js/dock.js                 | 22 +++++++++++++++-------
 scripts/clickthrough-showroom.mjs | 24 ++++++++++++++++++++----
 2 files changed, 35 insertions(+), 11 deletions(-)

diff --git a/public/js/dock.js b/public/js/dock.js
index f0fe1a9..ae4fa16 100644
--- a/public/js/dock.js
+++ b/public/js/dock.js
@@ -161,6 +161,9 @@
       ModalRig.rig(wrap, { key: 'qhdock:' + p.sel, handle: head, width: p.w, height: p.h });
     }
     wrap.classList.remove('qh-stowed');
+    el.classList.remove('qh-stowed');   // the PANEL itself was stowed (display:none!important) on boot;
+    //                                     removing it from the wrap alone left every launched panel's
+    //                                     CONTENT invisible/0×0 — only the header showed. (Phase-5 /5x.)
     bringToFront(p.sel, wrap);
     if (openOrder.indexOf(p.sel) === -1) openOrder.push(p.sel);
     saveOpen(); reflectMenu();
@@ -212,8 +215,12 @@
                 '<span>' + p.label + '</span><span class="qh-dot">●</span></button>';
     });
     html += '<div class="qh-menu-eyebrow">Tools</div>';
-    TOOLS.forEach(function (t) {
-      html += '<button class="qh-menu-item" data-tool="' + t.sel + '">' +
+    // NOTE: a TOOL selector may contain quotes/spaces (e.g. #top-bar [data-section="wings"]).
+    // Storing the raw selector in a data-* attribute breaks the HTML (inner quotes close the
+    // attribute early) AND breaks querySelector('[data-tool="<sel>"]') ("is not a valid
+    // selector"). So we key tools by INDEX and resolve TOOLS[idx].sel in JS. (Phase-5 /5x.)
+    TOOLS.forEach(function (t, i) {
+      html += '<button class="qh-menu-item" data-tool-idx="' + i + '">' +
                 '<span>' + t.label + '</span><span class="qh-dot">●</span></button>';
     });
     menu.innerHTML = html;
@@ -221,12 +228,13 @@
     menu.addEventListener('click', function (e) {
       var item = e.target.closest('.qh-menu-item'); if (!item) return;
       var pSel = item.getAttribute('data-panel');
-      var tSel = item.getAttribute('data-tool');
+      var tIdx = item.getAttribute('data-tool-idx');
       if (pSel) {
         var p = PANELS.filter(function (x) { return x.sel === pSel; })[0];
         if (p) toggle(p);
-      } else if (tSel) {
-        var btn = $(tSel);
+      } else if (tIdx !== null) {
+        var t = TOOLS[+tIdx];
+        var btn = t && $(t.sel);
         if (btn) { btn.click(); setTimeout(reflectMenu, 30); }
       }
     });
@@ -250,8 +258,8 @@
       var it = menu.querySelector('[data-panel="' + p.sel + '"]');
       if (it) it.classList.toggle('active', isOpen(p));
     });
-    TOOLS.forEach(function (t) {
-      var it = menu.querySelector('[data-tool="' + t.sel + '"]');
+    TOOLS.forEach(function (t, i) {
+      var it = menu.querySelector('[data-tool-idx="' + i + '"]');   // index key — selector may contain quotes
       var btn = $(t.sel);
       if (it) it.classList.toggle('active', !!btn && btn.classList.contains('active'));
     });
diff --git a/scripts/clickthrough-showroom.mjs b/scripts/clickthrough-showroom.mjs
index d0c0324..fdf24dc 100644
--- a/scripts/clickthrough-showroom.mjs
+++ b/scripts/clickthrough-showroom.mjs
@@ -93,7 +93,21 @@ const TOOLS = [
   }
   await page.screenshot({ path: path.join(OUT, 'panels-open.png') });
 
+  // helper: stow every modal, then launch ONLY the named panel (so it's on top + clickable,
+  // not buried under other open modals — that stacking caused false timeouts in sweep 2).
+  async function openOnly(panelSel) {
+    await page.evaluate(() => document.querySelectorAll('.qh-modal').forEach(m => m.classList.add('qh-stowed')));
+    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="${panelSel}"]`);
+    if (item) { await item.click(); await sleep(400); }
+    // close the launcher menu so it doesn't overlap the panel
+    await page.evaluate(() => { const m = document.getElementById('qh-menu'); if (m && m.classList.contains('open')) document.getElementById('qh-burger').click(); });
+    await sleep(200);
+  }
+
   // 3. exercise Controls (#window-bar): sort select all options + ranges
+  await openOnly('#window-bar');
   const sortSel = '#sort-select';
   if (await page.$(sortSel)) {
     const opts = await page.$$eval(sortSel + ' option', os => os.map(o => o.value));
@@ -118,7 +132,8 @@ const TOOLS = [
     } catch (e) { note('range', `${rng} threw ${e.message.split('\n')[0]}`); }
   }
 
-  // 4. Navigate buttons
+  // 4. Navigate buttons (open only the Navigate panel so it isn't buried)
+  await openOnly('#guided-bar');
   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`); }
@@ -128,11 +143,12 @@ const TOOLS = [
   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) {
+  // 5. proxy-click each tool from the menu (keyed by index — selectors may contain quotes)
+  for (let ti = 0; ti < TOOLS.length; ti++) {
+    const t = TOOLS[ti];
     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, '\\"')}"]`);
+    const item = await page.$(`.qh-menu-item[data-tool-idx="${ti}"]`);
     if (!item) { note('tool:' + t, 'menu item missing'); continue; }
     const before = consoleErrors.length;
     try { await item.click({ timeout: 4000 }); await sleep(450); }

← 52d16fc snapshot: clickthrough script + 5x report from review pass  ·  back to Quadrille Showroom  ·  /5x REPORT: 2 dock.js bugs fixed, 2 clean sweeps 2bef6b4 →