← back to CelebritySignatures
screenrecord/murals-debug.mjs
192 lines
// 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');