[object Object]

← back to Wallco Ai

theme stepper: live-marker via /api/pdp-theme + functional control verifier

b2e1d06be8cd1b00e5475cfa218aad644d1ec2bb · 2026-06-01 17:26:59 -0700 · Steve Abrams

Files touched

Diff

commit b2e1d06be8cd1b00e5475cfa218aad644d1ec2bb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 1 17:26:59 2026 -0700

    theme stepper: live-marker via /api/pdp-theme + functional control verifier
---
 public/theme-variants/theme-variant-nav.js |  10 +--
 scripts/verify-theme-controls.js           | 108 +++++++++++++++++++++++++++++
 2 files changed, 111 insertions(+), 7 deletions(-)

diff --git a/public/theme-variants/theme-variant-nav.js b/public/theme-variants/theme-variant-nav.js
index 45f032b..1ca47b5 100644
--- a/public/theme-variants/theme-variant-nav.js
+++ b/public/theme-variants/theme-variant-nav.js
@@ -110,15 +110,11 @@
       if (e.altKey && e.key === 'ArrowLeft')  { e.preventDefault(); go(-1); }
       if (e.altKey && e.key === 'ArrowRight') { e.preventDefault(); go(1); }
     });
-    // mark the live theme
-    fetch('/data/pdp-theme.json', { credentials: 'same-origin' })
+    // mark the live theme (server resolves null -> "best" and returns a slug)
+    fetch('/api/pdp-theme', { credentials: 'same-origin' })
       .then(function (r) { return r.ok ? r.json() : null; })
       .then(function (j) {
-        if (!j) return;
-        var raw = String(j.theme || j.variant || j.slug || j.live || j.best || '').toLowerCase().trim();
-        if (!raw) return;
-        if (raw === 'best') raw = 'best';
-        var s = raw.replace(/\.html?$/, '').replace(/^v\d+-/, ''); // v11-best -> best
+        var s = j && typeof j.theme === 'string' ? j.theme.toLowerCase().trim() : '';
         var li = SLUGS.indexOf(s);
         if (li !== -1) { LIVE_IDX = li; var l = document.getElementById('tvn-label'); if (l) l.innerHTML = label(); }
       })
diff --git a/scripts/verify-theme-controls.js b/scripts/verify-theme-controls.js
new file mode 100644
index 0000000..fae11e0
--- /dev/null
+++ b/scripts/verify-theme-controls.js
@@ -0,0 +1,108 @@
+#!/usr/bin/env node
+/* verify-theme-controls.js — functional check of the admin theme stepper +
+ * section drag-reorder on the PDP variants. Exercises behavior, not just load.
+ *   node scripts/verify-theme-controls.js <designId> <BASE>
+ * Exit 0 = all pass, 1 = a control is broken, 3 = can't run (no playwright/browser). */
+'use strict';
+const EXIT_SKIP = 3;
+function loadChromium() {
+  for (const t of ['playwright', '/Users/stevestudio2/Projects/animals/node_modules/playwright']) {
+    try { return require(t).chromium; } catch {}
+  }
+  console.error('SKIP: playwright not found'); process.exit(EXIT_SKIP);
+}
+const ID = process.argv[2] || '43200';
+const BASE = process.argv[3] || 'https://wallco.ai';
+let PASS = 0, FAIL = 0; const fails = [];
+const ok = n => { PASS++; console.log('  ✓ ' + n); };
+const bad = (n, w) => { FAIL++; fails.push(n + ' — ' + w); console.log('  ✗ ' + n + ' — ' + w); };
+
+(async () => {
+  const chromium = loadChromium();
+  let browser;
+  try { browser = await chromium.launch(); }
+  catch { try { browser = await chromium.launch({ channel: 'chrome' }); } catch (e) { console.error('SKIP: no browser'); process.exit(EXIT_SKIP); } }
+  const ctx = await browser.newContext({ viewport: { width: 1400, height: 1100 } });
+  const page = await ctx.newPage();
+  const errs = []; page.on('pageerror', e => errs.push(e.message));
+
+  console.log(`\nTheme-controls verify — base=${BASE} design=#${ID}\n`);
+
+  // ---- STEPPER ----
+  console.log('Stepper (admin):');
+  await page.goto(`${BASE}/design/${ID}/best?admin=1`, { waitUntil: 'domcontentloaded', timeout: 25000 });
+  await page.waitForFunction(() => !/Loading design/i.test(document.body.innerText || ''), { timeout: 15000 });
+  await page.waitForTimeout(400);
+  (await page.$('#tvn')) ? ok('stepper pill present (admin)') : bad('stepper pill', 'missing');
+  const label = await page.evaluate(() => { const e = document.querySelector('#tvn-label'); return e ? e.textContent.trim() : ''; });
+  /Best/.test(label) && /11\/17/.test(label) ? ok('label shows "Best · 11/17" (' + label + ')') : bad('stepper label', label);
+  // live dot (best is the live theme)
+  await page.waitForTimeout(600);
+  (await page.$('#tvn .tvn-live')) ? ok('live-theme dot shows on Best') : bad('live dot', 'absent (api/pdp-theme?)');
+  // NEXT navigates to roomsplit (slug after best)
+  await page.click('#tvn-next');
+  await page.waitForFunction(() => /\/design\/[^/]+\/roomsplit/.test(location.pathname), { timeout: 8000 }).catch(() => {});
+  /\/roomsplit$/.test(new URL(page.url()).pathname) ? ok('next → /design/' + ID + '/roomsplit') : bad('next nav', page.url());
+  // admin flag persisted across nav, so stepper still there
+  await page.waitForTimeout(300);
+  (await page.$('#tvn')) ? ok('stepper persists after navigation') : bad('stepper persist', 'gone');
+
+  // non-admin: stepper must NOT appear
+  const anon = await ctx.newPage();
+  await anon.goto(`${BASE}/design/${ID}/best?admin=0`, { waitUntil: 'domcontentloaded', timeout: 25000 });
+  await anon.waitForTimeout(500);
+  (await anon.$('#tvn')) ? bad('admin gate', 'stepper shown to non-admin!') : ok('hidden for non-admin (gate works)');
+  await anon.close();
+
+  // ---- DRAG REORDER ----
+  console.log('\nSection rearrange (admin):');
+  const p2 = await ctx.newPage();
+  await p2.goto(`${BASE}/design/${ID}/best?admin=1`, { waitUntil: 'domcontentloaded', timeout: 25000 });
+  await p2.waitForFunction(() => !/Loading design/i.test(document.body.innerText || ''), { timeout: 15000 });
+  await p2.waitForSelector('.tsr-sec', { timeout: 8000 }).catch(() => {});
+  (await p2.$('#tsr-pill')) ? ok('rearrange pill present') : bad('rearrange pill', 'missing');
+  const zoneInfo = await p2.evaluate(() => {
+    const zones = Array.from(document.querySelectorAll('.tsr-zone'));
+    return { zones: zones.length, handles: document.querySelectorAll('.tsr-handle').length,
+      firstZoneKids: zones.length ? Array.from(zones[0].children).filter(c => c.classList.contains('tsr-sec')).length : 0 };
+  });
+  zoneInfo.zones >= 1 ? ok('≥1 drag zone (' + zoneInfo.zones + ')') : bad('drag zones', '0 found');
+  zoneInfo.handles >= 2 ? ok('handles on sections (' + zoneInfo.handles + ')') : bad('handles', zoneInfo.handles);
+
+  // capture order, drag first section's handle onto the 3rd section, expect order change + persistence
+  const before = await p2.evaluate(() => {
+    const z = document.querySelector('.tsr-zone'); if (!z) return null;
+    return Array.from(z.children).filter(c => c.classList.contains('tsr-sec')).map(c => c.className.replace(/\s*tsr-\S+/g, '').trim());
+  });
+  if (before && before.length >= 3) {
+    const handles = await p2.$$('.tsr-zone > .tsr-sec > .tsr-handle');
+    if (handles.length >= 3) {
+      await handles[0].dragTo(handles[2]);
+      await p2.waitForTimeout(300);
+    }
+    const after = await p2.evaluate(() => {
+      const z = document.querySelector('.tsr-zone');
+      return Array.from(z.children).filter(c => c.classList.contains('tsr-sec')).map(c => c.className.replace(/\s*tsr-\S+/g, '').trim());
+    });
+    JSON.stringify(after) !== JSON.stringify(before) ? ok('drag reorders sections') : bad('drag reorder', 'order unchanged');
+    const saved = await p2.evaluate(() => localStorage.getItem('wallco:secorder:best'));
+    saved && saved.length > 2 ? ok('order persisted to localStorage') : bad('persist', 'nothing saved');
+    // reload → saved order re-applied
+    await p2.reload({ waitUntil: 'domcontentloaded' });
+    await p2.waitForSelector('.tsr-sec', { timeout: 8000 }).catch(() => {});
+    await p2.waitForTimeout(400);
+    const afterReload = await p2.evaluate(() => {
+      const z = document.querySelector('.tsr-zone');
+      return z ? Array.from(z.children).filter(c => c.classList.contains('tsr-sec')).map(c => c.className.replace(/\s*tsr-\S+/g, '').trim()) : [];
+    });
+    JSON.stringify(afterReload) === JSON.stringify(after) ? ok('saved order re-applied on reload') : bad('reload apply', 'order not restored');
+  } else {
+    bad('drag setup', 'zone had <3 sections (' + (before ? before.length : 0) + ')');
+  }
+
+  errs.length ? bad('page errors', errs[0]) : ok('no page errors');
+  await browser.close();
+  console.log(`\n──────── ${PASS} passed, ${FAIL} failed ────────`);
+  if (FAIL) { console.log('FAILURES:'); fails.forEach(f => console.log('  • ' + f)); process.exit(1); }
+  console.log('All theme-control checks passed.');
+})().catch(e => { console.error('VERIFY HARNESS ERROR:', e); process.exit(1); });

← 957c89c theme-variants: admin theme stepper (slug-based) + drag-reor  ·  back to Wallco Ai  ·  gitignore: stop tracking large regeneratable data blobs (ets 7fa37ed →