← 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
M public/theme-variants/theme-variant-nav.jsA scripts/verify-theme-controls.js
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 →