← back to CelebritySignatures
screenrecord: track murals debug + verify harnesses (TK-10193 reproducibility)
43d50a8d1e8c5f1bd6a77d505530f459d0482edd · 2026-08-08 16:08:24 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A screenrecord/murals-debug.mjsA screenrecord/verify-murals.mjsA screenrecord/verify2-murals.mjs
Diff
commit 43d50a8d1e8c5f1bd6a77d505530f459d0482edd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 8 16:08:24 2026 -0700
screenrecord: track murals debug + verify harnesses (TK-10193 reproducibility)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
screenrecord/murals-debug.mjs | 191 ++++++++++++++++++++++++++++++++++++++++
screenrecord/verify-murals.mjs | 55 ++++++++++++
screenrecord/verify2-murals.mjs | 62 +++++++++++++
3 files changed, 308 insertions(+)
diff --git a/screenrecord/murals-debug.mjs b/screenrecord/murals-debug.mjs
new file mode 100644
index 0000000..9c3103a
--- /dev/null
+++ b/screenrecord/murals-debug.mjs
@@ -0,0 +1,191 @@
+// TK-10193 — 5-pass screen-record UI debug of CelebritySignatures /murals storefront.
+// Boots against a locally-running server in Stripe TEST mode. Guards payBtn so the
+// checkout is OBSERVED (network response) but the page never navigates to Stripe / pays.
+import { createRequire } from 'node:module';
+import fs from 'fs';
+const require = createRequire(import.meta.url);
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+
+const PORT = fs.readFileSync('/tmp/cs_murals_port', 'utf8').trim();
+const URL = `http://127.0.0.1:${PORT}/murals`;
+const DIR = 'screenrecord';
+const LOG = `${DIR}/debug-log-murals.jsonl`;
+
+const prior = fs.existsSync(LOG)
+ ? fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean)
+ : [];
+const priorErrs = new Set(prior.filter(r => r.errors && r.errors.length).map(r => r.selector));
+const append = o => fs.appendFileSync(LOG, JSON.stringify(o) + '\n');
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+// Build the interaction plan for a run. Each returns {sel,label,type} steps.
+function planFor(run) {
+ const base = [
+ { sel: '#finderInput', label: 'signature-finder-type', type: 'type', value: 'a' },
+ { sel: '#selMural', label: 'select-mural-dropdown', type: 'selectFirst' },
+ { sel: '#scenePresets button', label: 'scene-preset', type: 'clickEach', cap: 6 },
+ { sel: '#wallW', label: 'wall-width-slider', type: 'range' },
+ { sel: '#wallH', label: 'wall-height-slider', type: 'range' },
+ { sel: '#wallWnum', label: 'wall-width-num', type: 'fillNum', value: '120' },
+ { sel: '#wallHnum', label: 'wall-height-num', type: 'fillNum', value: '96' },
+ { sel: '#murW', label: 'mural-width-slider', type: 'range' },
+ { sel: '#murH', label: 'mural-height-slider', type: 'range' },
+ { sel: '#btnCenter', label: 'center-mural-btn', type: 'click' },
+ { sel: '.gallery .place', label: 'gallery-place-mural', type: 'clickFirst' },
+ { sel: '.gallery .r-item', label: 'gallery-signature-item', type: 'clickFirst' },
+ { sel: '#provModal', label: 'provenance-modal-open+escape', type: 'provModal' },
+ { sel: '#orderForm', label: 'order-form-fill+submit', type: 'orderForm' },
+ { sel: '#payBtn', label: 'pay-and-order-checkout', type: 'pay' },
+ ];
+ if (run === 0) return base; // DOM/logical order
+ if (run === 1) return [...base].reverse(); // reverse
+ if (run === 2) return [...base].sort((a, b) => (b.type === 'range') - (a.type === 'range')); // sliders first
+ if (run === 3) { const s = [...base]; for (let i = s.length - 1; i > 0; i--) { const j = (i * 7 + 3 * 13) % (i + 1); [s[i], s[j]] = [s[j], s[i]]; } return s; } // seeded shuffle
+ if (run === 4) return [...base].sort((a, b) => (priorErrs.has(b.sel)) - (priorErrs.has(a.sel))); // re-hit prior errors first
+ return base;
+}
+
+async function doStep(page, run, step) {
+ const errs = [];
+ const grab = () => errs.splice(0);
+ let ok = false, effect = null;
+ try {
+ switch (step.type) {
+ case 'type': {
+ const el = page.locator(step.sel).first();
+ await el.scrollIntoViewIfNeeded({ timeout: 2000 });
+ await el.fill(step.value, { timeout: 2000 });
+ await sleep(400);
+ const resCount = await page.locator('#finderRes .r-item, #finderRes *').count().catch(() => 0);
+ ok = true; effect = `finderRes children=${resCount}`;
+ break;
+ }
+ case 'selectFirst': {
+ const el = page.locator(step.sel).first();
+ const opts = await el.locator('option').count().catch(() => 0);
+ if (opts > 0) { await el.selectOption({ index: Math.min(1, opts - 1) }).catch(async () => { await el.selectOption({ index: 0 }); }); await sleep(500); ok = true; effect = `options=${opts}`; }
+ else { effect = 'no options'; }
+ break;
+ }
+ case 'clickEach': {
+ const loc = page.locator(step.sel);
+ const n = Math.min(await loc.count(), step.cap || 99);
+ for (let i = 0; i < n; i++) { await loc.nth(i).click({ timeout: 2000 }).catch(() => {}); await sleep(250); }
+ ok = n > 0; effect = `clicked=${n}`;
+ break;
+ }
+ case 'range': {
+ const el = page.locator(step.sel).first();
+ await el.scrollIntoViewIfNeeded({ timeout: 2000 });
+ const before = await el.inputValue().catch(() => null);
+ // drive the range across low/mid/high
+ for (const frac of [0.1, 0.5, 0.9, run % 2 ? 0.3 : 0.75]) {
+ await el.evaluate((n, f) => { const min = +n.min || 0, max = +n.max || 100; n.value = Math.round(min + (max - min) * f); n.dispatchEvent(new Event('input', { bubbles: true })); n.dispatchEvent(new Event('change', { bubbles: true })); }, frac);
+ await sleep(150);
+ }
+ const after = await el.inputValue().catch(() => null);
+ ok = true; effect = `${before}->${after}`;
+ break;
+ }
+ case 'fillNum': {
+ const el = page.locator(step.sel).first();
+ await el.fill(step.value, { timeout: 2000 }); await el.dispatchEvent('change').catch(() => {}); await sleep(300);
+ const v = await el.inputValue().catch(() => null); ok = true; effect = `val=${v}`;
+ break;
+ }
+ case 'click': {
+ await page.locator(step.sel).first().click({ timeout: 2000 }); await sleep(300); ok = true;
+ break;
+ }
+ case 'clickFirst': {
+ const loc = page.locator(step.sel); const n = await loc.count();
+ if (n > 0) { await loc.first().click({ timeout: 2000 }).catch(() => {}); await sleep(500); ok = true; effect = `count=${n}`; }
+ else effect = 'none present';
+ break;
+ }
+ case 'provModal': {
+ // open via a gallery provenance trigger if present, else force-open, then close paths
+ const opened = await page.evaluate(() => { const m = document.querySelector('#provModal'); if (!m) return false; m.classList.add('open'); return m.classList.contains('open'); });
+ await sleep(300);
+ await page.keyboard.press('Escape'); await sleep(200);
+ const closedAfterEsc = await page.evaluate(() => !document.querySelector('#provModal')?.classList.contains('open'));
+ // reopen + backdrop close
+ await page.evaluate(() => document.querySelector('#provModal')?.classList.add('open')); await sleep(150);
+ await page.locator('#provX').click({ timeout: 1500 }).catch(() => {});
+ const closedAfterX = await page.evaluate(() => !document.querySelector('#provModal')?.classList.contains('open'));
+ ok = opened; effect = `open=${opened} escClose=${closedAfterEsc} xClose=${closedAfterX}`;
+ break;
+ }
+ case 'orderForm': {
+ // fill any text/email inputs inside the order form, then submit
+ const form = page.locator('#orderForm');
+ const inputs = form.locator('input, textarea');
+ const n = await inputs.count();
+ for (let i = 0; i < n; i++) {
+ const inp = inputs.nth(i);
+ const type = (await inp.getAttribute('type')) || 'text';
+ if (type === 'checkbox' || type === 'radio') { await inp.check().catch(() => {}); }
+ else if (type === 'email') { await inp.fill('screenrec-test@example.com').catch(() => {}); }
+ else if (type === 'number' || type === 'range') { /* leave */ }
+ else { await inp.fill('Rec Test').catch(() => {}); }
+ }
+ await form.locator('button[type=submit], [type=submit]').first().click({ timeout: 2000 }).catch(() => {});
+ await sleep(700);
+ const msg = await page.locator('#orderMsg').textContent().catch(() => '');
+ ok = true; effect = `orderMsg="${(msg || '').trim().slice(0, 80)}"`;
+ break;
+ }
+ case 'pay': {
+ // capture the checkout API response WITHOUT completing payment. Navigation off-host is blocked by the route guard.
+ let apiResp = null;
+ const onResp = async r => { if (r.url().includes('/api/mural-checkout')) { apiResp = { status: r.status() }; try { apiResp.body = (await r.text()).slice(0, 200); } catch {} } };
+ page.on('response', onResp);
+ await page.locator('#payBtn').first().click({ timeout: 2500 }).catch(() => {});
+ await sleep(1500);
+ page.off('response', onResp);
+ const payMsg = await page.locator('#orderMsg, #spec').first().textContent().catch(() => '');
+ ok = true; effect = `checkout=${JSON.stringify(apiResp)} onPage=${page.url().includes(':' + PORT)} msg="${(payMsg || '').trim().slice(0, 60)}"`;
+ break;
+ }
+ }
+ } catch (e) { errs.push('' + e); }
+ append({ run, ts: new Date().toISOString(), selector: step.sel, label: step.label, action: step.type, ok, effect, errors: grab() });
+ return { ok, effect, errors: errs };
+}
+
+for (let run = 0; run < 5; run++) {
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext({ viewport: { width: 1600, height: 900 }, recordVideo: { dir: `${DIR}/rec-murals/run${run}` } });
+ const page = await ctx.newPage();
+ const live = [];
+ page.on('console', m => { if (m.type() === 'error') live.push(m.text()); });
+ page.on('pageerror', e => live.push('pageerror: ' + e));
+ // GUARD: block any navigation off the local host (e.g. real Stripe checkout redirect) — observe only, never pay.
+ await ctx.route('**/*', route => {
+ const u = route.request().url();
+ const isNav = route.request().isNavigationRequest();
+ if (isNav && !u.includes(`127.0.0.1:${PORT}`) && !u.startsWith('data:') && !u.startsWith('about:')) {
+ append({ run, ts: new Date().toISOString(), selector: 'NAV-GUARD', label: 'blocked-offhost-nav', action: 'abort', ok: true, effect: `blocked ${u.slice(0, 120)}`, errors: [] });
+ return route.abort();
+ }
+ return route.continue();
+ });
+
+ const t0 = Date.now();
+ await page.goto(URL, { waitUntil: 'domcontentloaded' });
+ await sleep(1200); // let /api/murals-catalog + /api/signatures render
+ // record page-load console errors
+ if (live.length) append({ run, ts: new Date().toISOString(), selector: 'PAGE-LOAD', label: 'load-console-errors', action: 'load', ok: false, effect: null, errors: live.splice(0) });
+
+ for (const step of planFor(run)) {
+ const r = await doStep(page, run, step);
+ // fold in any console errors that fired during the step but arrived async
+ if (live.length) { const e = live.splice(0); append({ run, ts: new Date().toISOString(), selector: step.sel, label: step.label + ':async-console', action: 'console', ok: false, effect: null, errors: e }); }
+ }
+ append({ run, ts: new Date().toISOString(), selector: 'RUN-END', label: 'run-complete', action: 'summary', ok: true, effect: `durationMs=${Date.now() - t0}`, errors: [] });
+ await ctx.close();
+ await browser.close();
+ console.log(`run ${run} done`);
+}
+console.log('ALL 5 RUNS COMPLETE');
diff --git a/screenrecord/verify-murals.mjs b/screenrecord/verify-murals.mjs
new file mode 100644
index 0000000..fcb20f6
--- /dev/null
+++ b/screenrecord/verify-murals.mjs
@@ -0,0 +1,55 @@
+import { createRequire } from 'node:module';
+import fs from 'fs';
+const require = createRequire(import.meta.url);
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+const PORT = fs.readFileSync('/tmp/cs_murals_port', 'utf8').trim();
+const BASE = `http://127.0.0.1:${PORT}`;
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+const b = await chromium.launch();
+const ctx = await b.newContext();
+const p = await ctx.newPage();
+await ctx.route('**/*', r => { const u = r.request().url(); if (r.request().isNavigationRequest() && !u.includes('127.0.0.1:' + PORT) && !u.startsWith('data:')) return r.abort(); return r.continue(); });
+const checkoutCalls = [];
+p.on('response', r => { if (r.url().includes('/api/mural-checkout')) checkoutCalls.push(r.status()); });
+
+await p.goto(BASE + '/murals', { waitUntil: 'domcontentloaded' });
+await sleep(1500);
+
+const gal = await p.evaluate(() => ({
+ dataPlace: document.querySelectorAll('#gallery [data-place]').length,
+ rosterTrig: document.querySelectorAll('#gallery [data-roster]').length,
+ selMuralOpts: document.querySelectorAll('#selMural option').length,
+}));
+console.log('GALLERY:', JSON.stringify(gal));
+
+// place a mural so `cur` is set (checkout needs cur.slug)
+await p.locator('#gallery [data-place]').first().click().catch(() => {});
+await sleep(600);
+
+// (B) fields intact, no prior submit -> Pay should fire checkout
+await p.locator('#orderForm [name=name]').fill('Rec Test').catch(() => {});
+await p.locator('#orderForm [name=email]').fill('t@example.com').catch(() => {});
+await p.locator('#payBtn').click().catch(() => {});
+await sleep(1300);
+const msgB = await p.locator('#orderMsg').textContent().catch(() => '');
+console.log('(B) PAY intact -> checkoutCalls=' + JSON.stringify(checkoutCalls) + ' msg="' + (msgB || '').trim() + '"');
+
+// (C) reproduce: refill, click "Request a quote instead" (submit -> f.reset()), then Pay
+checkoutCalls.length = 0;
+await p.locator('#orderForm [name=name]').fill('Rec Test').catch(() => {});
+await p.locator('#orderForm [name=email]').fill('t@example.com').catch(() => {});
+await p.locator('#orderForm button[type=submit]').click().catch(() => {});
+await sleep(900);
+const afterSubmit = await p.evaluate(() => ({
+ name: document.querySelector('#orderForm [name=name]').value,
+ email: document.querySelector('#orderForm [name=email]').value,
+ msg: (document.querySelector('#orderMsg').textContent || '').trim(),
+}));
+console.log('(C) after quote-submit fields:', JSON.stringify(afterSubmit));
+await p.locator('#payBtn').click().catch(() => {});
+await sleep(900);
+const msgC = await p.locator('#orderMsg').textContent().catch(() => '');
+console.log('(C) PAY after submit -> checkoutCalls=' + JSON.stringify(checkoutCalls) + ' msg="' + (msgC || '').trim() + '"');
+
+await b.close();
diff --git a/screenrecord/verify2-murals.mjs b/screenrecord/verify2-murals.mjs
new file mode 100644
index 0000000..02d9a1b
--- /dev/null
+++ b/screenrecord/verify2-murals.mjs
@@ -0,0 +1,62 @@
+import { createRequire } from 'node:module';
+import fs from 'fs';
+const require = createRequire(import.meta.url);
+const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
+const PORT = fs.readFileSync('/tmp/cs_murals_port', 'utf8').trim();
+const BASE = `http://127.0.0.1:${PORT}`;
+const OUT = 'screenrecord/verify2-out.txt';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+const out = [];
+const say = s => { out.push(s); fs.writeFileSync(OUT, out.join('\n') + '\n'); };
+
+async function freshPage(ctx) {
+ const p = await ctx.newPage();
+ await p.goto(BASE + '/murals', { waitUntil: 'domcontentloaded' });
+ await sleep(1500);
+ await p.locator('#gallery [data-place]').first().click().catch(() => {}); // sets `cur`
+ await sleep(500);
+ return p;
+}
+
+const b = await chromium.launch();
+const ctx = await b.newContext();
+await ctx.route('**/*', r => { const u = r.request().url(); if (r.request().isNavigationRequest() && !u.includes('127.0.0.1:' + PORT) && !u.startsWith('data:')) return r.abort(); return r.continue(); });
+
+// PHASE B: fields intact -> Pay fires checkout
+{
+ const p = await freshPage(ctx);
+ const calls = [];
+ p.on('response', async r => { if (r.url().includes('/api/mural-checkout')) { let body = ''; try { body = (await r.text()).slice(0, 150); } catch {} calls.push({ status: r.status(), body }); } });
+ await p.locator('#orderForm [name=name]').fill('Rec Test').catch(() => {});
+ await p.locator('#orderForm [name=email]').fill('t@example.com').catch(() => {});
+ const msgBefore = await p.locator('#orderMsg').textContent().catch(() => '');
+ await p.locator('#payBtn').click().catch(() => {});
+ await sleep(1500);
+ say('(B) PAY with fields intact -> checkoutCalls=' + JSON.stringify(calls) + ' | orderMsgBeforeNavGuard="' + (msgBefore || '').trim() + '"');
+ await p.close().catch(() => {});
+}
+
+// PHASE C: submit quote (resets fields) THEN pay -> observe guard message
+{
+ const p = await freshPage(ctx);
+ const calls = [];
+ p.on('response', r => { if (r.url().includes('/api/mural-checkout')) calls.push(r.status()); });
+ await p.locator('#orderForm [name=name]').fill('Rec Test').catch(() => {});
+ await p.locator('#orderForm [name=email]').fill('t@example.com').catch(() => {});
+ await p.locator('#orderForm button[type=submit]').click().catch(() => {}); // "Request a quote instead"
+ await sleep(1000);
+ const fields = await p.evaluate(() => {
+ const n = document.querySelector('#orderForm [name=name]');
+ const e = document.querySelector('#orderForm [name=email]');
+ return { name: n ? n.value : 'NULL', email: e ? e.value : 'NULL', msg: (document.querySelector('#orderMsg').textContent || '').trim() };
+ }).catch(e => ({ err: '' + e }));
+ say('(C) after "Request a quote instead": fields=' + JSON.stringify(fields));
+ await p.locator('#payBtn').click().catch(() => {});
+ await sleep(900);
+ const msgC = await p.locator('#orderMsg').textContent().catch(() => '');
+ say('(C) then PAY -> checkoutCalls=' + JSON.stringify(calls) + ' | orderMsg="' + (msgC || '').trim() + '"');
+ await p.close().catch(() => {});
+}
+
+await b.close();
+say('DONE');
← 57895fb game: harden — hide #prep before showRound() (Cody gate, TK-
·
back to CelebritySignatures
·
mobile: iPhone-only (supportsTablet:false) — drops iPad so A a613dfa →