← 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');