← back to CelebritySignatures

scripts/screenrecord.mjs

616 lines

// Celebrity Signatures — screen-record + UI debug
// 5 runs × distinct click-order combinations
// Output: screenrecord/rec/run{0..4}/*.mp4
// Log:    screenrecord/debug-log.jsonl (append-only, cross-reload memory)

import pkg from '/Users/macstudio3/.npm-global/lib/node_modules/playwright/index.js';
const { chromium } = pkg;
import fs from 'fs';
import { execSync } from 'child_process';

const ROOT = '/Users/macstudio3/Projects/CelebritySignatures';
const LOG = `${ROOT}/screenrecord/debug-log.jsonl`;
const RECDIR = `${ROOT}/screenrecord/rec`;
const URL = 'http://localhost:9920/';

// ── Read prior runs ──
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 priorErrors = prior.filter(r => r.errors && r.errors.length > 0);
const priorErrorSelectors = new Set(priorErrors.map(e => e.selector).filter(Boolean));
console.log(`Prior log: ${prior.length} entries, ${priorErrors.length} with errors`);
if (priorErrorSelectors.size) console.log('Prior errors on:', [...priorErrorSelectors].join(', '));

const append = (o) => fs.appendFileSync(LOG, JSON.stringify(o) + '\n');

const ORDERED_ELS_RUN_DESCRIPTIONS = [
  'DOM order (natural)',
  'Reverse DOM order',
  'Sliders-first',
  'Seeded shuffle (seed=42)',
  'Errored-elements-first (from prior runs)',
];

// ── Seeded shuffle ──
function seededShuffle(arr, seed) {
  const s = [...arr];
  let state = seed;
  const rand = () => { state = (state * 1664525 + 1013904223) & 0xffffffff; return (state >>> 0) / 0x100000000; };
  for (let i = s.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [s[i], s[j]] = [s[j], s[i]]; }
  return s;
}

function orderFor(run, els) {
  if (run === 0) return [...els];
  if (run === 1) return [...els].reverse();
  if (run === 2) return [...els].sort((a, b) => (b.action === 'range' ? 1 : 0) - (a.action === 'range' ? 1 : 0));
  if (run === 3) return seededShuffle([...els], 42);
  if (run === 4) return [...els].sort((a, b) => (priorErrorSelectors.has(b.sel) ? 1 : 0) - (priorErrorSelectors.has(a.sel) ? 1 : 0));
  return [...els];
}

// ── Helpers ──
async function waitForData(page) {
  await page.waitForFunction(() => document.querySelectorAll('.card').length > 0, { timeout: 15000 });
}

async function ensureDrawerOpen(page) {
  const isOpen = await page.evaluate(() => document.querySelector('#drawer')?.classList.contains('open'));
  if (!isOpen) {
    await page.click('#burger');
    await page.waitForTimeout(350);
  }
  // Ensure we're on grid tab so controls are visible
  const controlsVisible = await page.evaluate(() => {
    const el = document.querySelector('#controls');
    return el && getComputedStyle(el).display !== 'none';
  });
  if (!controlsVisible) {
    // Switch to grid tab to restore controls
    const gridTab = await page.$('.drawer .tab[data-v="grid"]');
    if (gridTab) { await gridTab.click(); await page.waitForTimeout(200); }
  }
}

async function ensureDrawerClosed(page) {
  const isOpen = await page.evaluate(() => document.querySelector('#drawer')?.classList.contains('open'));
  if (isOpen) {
    await page.click('#drawerX');
    await page.waitForTimeout(300);
  }
}

async function safeScrollIntoView(page, locator) {
  try {
    await locator.scrollIntoViewIfNeeded({ timeout: 2000 });
  } catch {
    // Fallback: JS scroll
    try {
      const el = await locator.elementHandle({ timeout: 500 });
      if (el) await el.evaluate(n => n.scrollIntoView({ block: 'center' }));
    } catch { /* ignore */ }
  }
}

// ── Popup hover-click helper ──
async function hoverAndClickFirstInfoChip(page) {
  await ensureDrawerClosed(page);
  // Scroll to top of page to find cards
  await page.evaluate(() => window.scrollTo(0, 0));
  await page.waitForTimeout(200);
  const card = page.locator('.card').first();
  await safeScrollIntoView(page, card);
  await card.hover();
  await page.waitForTimeout(250);
  const chip = card.locator('.info-chip');
  await chip.click({ timeout: 3000 });
  await page.waitForTimeout(600);
}

// ── Static element list ──
const STATIC_ELEMENTS = [
  // TOPBAR
  { sel: 'a.logo',                                     label: 'Logo link (home)',                        action: 'nav-check' },
  { sel: '#q',                                         label: 'Search: type "monet"',                    action: 'type',    value: 'monet' },
  { sel: '#q',                                         label: 'Search: type "kahlo"',                    action: 'type',    value: 'kahlo' },
  { sel: '#q',                                         label: 'Search: type "xyzzy" (no results)',       action: 'type',    value: 'xyzzy' },
  { sel: '#q',                                         label: 'Search: clear',                           action: 'clear' },
  { sel: 'nav.gnav a[href="/murals"]',                 label: 'Nav: Murals',                             action: 'nav-check' },
  { sel: 'nav.gnav a[href="/game"]',                   label: 'Nav: Games',                              action: 'nav-check' },
  { sel: 'nav.gnav a[href="/upload"]',                 label: 'Nav: Upload yours',                       action: 'nav-check' },
  { sel: '#burger',                                    label: 'Hamburger — open drawer',                 action: 'click' },
  // HERO CTAs
  { sel: 'a.hbtn[href="#collection"]',                 label: 'Hero CTA: Browse the collection',         action: 'anchor-click' },
  { sel: 'a.hbtn[href="/murals"]',                     label: 'Hero CTA: Order a mural',                 action: 'nav-check' },
  { sel: 'a.hbtn[href="/game"]',                       label: 'Hero CTA: Play the games',                action: 'nav-check' },
  // DRAWER close
  { sel: '#drawerX',                                   label: 'Drawer × close',                          action: 'click', prereq: 'openDrawer' },
  { sel: '#dback',                                     label: 'Drawer backdrop',                         action: 'click', prereq: 'openDrawer' },
  // DRAWER tabs
  { sel: '.drawer .tab[data-v="grid"]',                label: 'Drawer tab: Grid',                        action: 'click', prereq: 'openDrawer' },
  { sel: '.drawer .tab[data-v="collages"]',            label: 'Drawer tab: Collages',                    action: 'click', prereq: 'openDrawer' },
  { sel: '.drawer .tab[data-v="grid"]',                label: 'Drawer tab: back to Grid',                action: 'click', prereq: 'openDrawer' },
  // DRAWER nav links (verified without navigation)
  { sel: '.d-links a[href="/murals"]',                 label: 'Drawer link: Signature murals',           action: 'nav-check' },
  { sel: '.d-links a[href="/game"]',                   label: 'Drawer link: The Signature Games',        action: 'nav-check' },
  { sel: '.d-links a[href="/upload"]',                 label: 'Drawer link: Upload your signature',      action: 'nav-check' },
  // DRAWER controls — all require drawer open + grid tab
  { sel: '#useFilter',                                 label: 'Use filter: Free to use',                 action: 'select', value: 'yes',                prereq: 'openDrawerGrid' },
  { sel: '#useFilter',                                 label: 'Use filter: Permission needed',           action: 'select', value: 'permission-needed',   prereq: 'openDrawerGrid' },
  { sel: '#useFilter',                                 label: 'Use filter: All signatures',              action: 'select', value: 'all',                prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Category then rank',                action: 'select', value: 'cat',                prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Name A→Z',                          action: 'select', value: 'name',              prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Name Z→A',                          action: 'select', value: 'name-desc',         prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Rank',                              action: 'select', value: 'rank',              prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Notability',                        action: 'select', value: 'notability',        prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Usable first',                      action: 'select', value: 'usable',            prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Risk low→high',                     action: 'select', value: 'risk',              prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Risk high→low',                     action: 'select', value: 'risk-desc',         prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: License A→Z',                       action: 'select', value: 'license',           prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Living first',                       action: 'select', value: 'living',            prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Deceased first',                    action: 'select', value: 'deceased',          prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Death date',                        action: 'select', value: 'death',             prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Museums',                           action: 'select', value: 'museums',           prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: Signature count',                   action: 'select', value: 'sigcount',          prereq: 'openDrawerGrid' },
  { sel: '#sort',                                      label: 'Sort: reset to cat',                      action: 'select', value: 'cat',               prereq: 'openDrawerGrid' },
  { sel: '#density',                                   label: 'Density slider → 3',                      action: 'range',  value: 3,                   prereq: 'openDrawerGrid' },
  { sel: '#density',                                   label: 'Density slider → 7',                      action: 'range',  value: 7,                   prereq: 'openDrawerGrid' },
  { sel: '#density',                                   label: 'Density slider → 5',                      action: 'range',  value: 5,                   prereq: 'openDrawerGrid' },
  // CATEGORY CHIPS
  { sel: '#catChips .chip[data-c="All"]',              label: 'Category: All',                           action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="Oldest Signatures"]',label: 'Category: Oldest Signatures',             action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="Declaration of Independence"]', label: 'Category: Declaration',        action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="Politics"]',         label: 'Category: Politics',                      action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="Artists"]',          label: 'Category: Artists',                       action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="Authors"]',          label: 'Category: Authors',                       action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#catChips .chip[data-c="All"]',              label: 'Category: back to All',                   action: 'click', prereq: 'openDrawerGrid' },
  // LAYOUT CHIPS
  { sel: '#layoutChips .chip[data-l="gallery"]',       label: 'Layout: Gallery',                         action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="float"]',         label: 'Layout: Float',                           action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="plaque"]',        label: 'Layout: Plaque',                          action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="ledger"]',        label: 'Layout: Ledger',                          action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="vitrine"]',       label: 'Layout: Vitrine',                         action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="wall"]',          label: 'Layout: Wall',                            action: 'click', prereq: 'openDrawerGrid' },
  { sel: '#layoutChips .chip[data-l="gallery"]',       label: 'Layout: back to Gallery',                 action: 'click', prereq: 'openDrawerGrid' },
  // SECTION HEAD BUTTON
  { sel: '#filtersHint',                               label: '☰ Categories/sort/views button',          action: 'click' },
  // ESCAPE to close drawer
  { sel: null,                                         label: 'Escape key closes drawer',                action: 'escape', prereq: 'openDrawerGrid' },
  // CARD → ARTIST POPUP
  { sel: '.info-chip',                                 label: 'First card Details chip → open popup',    action: 'click-details' },
  { sel: '.ap-x',                                      label: 'Artist popup × close',                    action: 'click' },
  { sel: '.info-chip',                                 label: 'Re-open artist popup (backdrop close)',    action: 'click-details' },
  { sel: '.ap-back',                                   label: 'Artist popup backdrop close',             action: 'click' },
  { sel: '.info-chip',                                 label: 'Re-open artist popup (escape close)',      action: 'click-details' },
  { sel: null,                                         label: 'Escape key closes artist popup',          action: 'escape' },
  // COLLAGES TAB (last, standalone)
  { sel: '.drawer .tab[data-v="collages"]',            label: 'Collages tab: switch + check',            action: 'click', prereq: 'openDrawer' },
  { sel: '.drawer .tab[data-v="grid"]',                label: 'Collages → Grid: switch back',            action: 'click', prereq: 'openDrawer' },
];

// ── Per-element action executor ──
async function doAction(page, el, errs, run) {
  const { sel, label, action, value, prereq } = el;
  const ts = new Date().toISOString();
  let ok = false;
  let effect = '';
  const capturedErrs = errs.splice(0);

  try {
    // Handle prereqs
    if (prereq === 'openDrawer') await ensureDrawerOpen(page);
    if (prereq === 'openDrawerGrid') {
      await ensureDrawerOpen(page);
      // Ensure we're on grid tab (controls visible)
      const onGrid = await page.evaluate(() => {
        const controls = document.querySelector('#controls');
        return controls && getComputedStyle(controls).display !== 'none';
      });
      if (!onGrid) {
        const gridTab = page.locator('.drawer .tab[data-v="grid"]').first();
        await gridTab.click({ timeout: 2000 });
        await page.waitForTimeout(200);
      }
    }

    if (action === 'nav-check') {
      const loc = page.locator(sel).first();
      const count = await loc.count();
      const href = count > 0 ? await loc.getAttribute('href') : null;
      const text = count > 0 ? (await loc.textContent())?.trim() : null;
      effect = `exists:${count>0}, href:${href}, text:${text}`;
      ok = count > 0;
    } else if (action === 'anchor-click') {
      // Scroll to anchor without navigating away
      const loc = page.locator(sel).first();
      const count = await loc.count();
      if (!count) { effect = 'element not found'; return; }
      await loc.scrollIntoViewIfNeeded({ timeout: 2000 }).catch(() => {});
      await loc.click({ timeout: 3000 });
      await page.waitForTimeout(500);
      const scrollY = await page.evaluate(() => window.scrollY);
      effect = `scrollY after: ${scrollY}`;
      ok = scrollY > 0;
    } else if (action === 'type') {
      const loc = page.locator(sel).first();
      await loc.fill(value);
      await page.waitForTimeout(350);
      const cardCount = await page.evaluate(() => document.querySelectorAll('.card').length);
      const emptyHidden = await page.evaluate(() => document.querySelector('#empty')?.hidden);
      effect = `typed "${value}"; cards:${cardCount}, empty hidden:${emptyHidden}`;
      ok = true;
    } else if (action === 'clear') {
      const loc = page.locator(sel).first();
      await loc.fill('');
      await page.waitForTimeout(300);
      const cardCount = await page.evaluate(() => document.querySelectorAll('.card').length);
      effect = `cleared; cards:${cardCount}`;
      ok = true;
    } else if (action === 'select') {
      const loc = page.locator(sel).first();
      const vis = await loc.isVisible({ timeout: 2000 }).catch(() => false);
      if (!vis) { effect = `select not visible (controls hidden?)`; ok = false; return; }
      await loc.selectOption(value, { timeout: 3000 });
      await page.waitForTimeout(300);
      const cardCount = await page.evaluate(() => document.querySelectorAll('.card').length);
      effect = `selected ${value}; cards:${cardCount}`;
      ok = true;
    } else if (action === 'range') {
      const loc = page.locator(sel).first();
      const vis = await loc.isVisible({ timeout: 2000 }).catch(() => false);
      if (!vis) { effect = `slider not visible`; ok = false; return; }
      await loc.fill(String(value));
      await loc.dispatchEvent('input');
      await page.waitForTimeout(200);
      const cols = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--cols').trim());
      effect = `set to ${value}; --cols:${cols}`;
      ok = cols.trim() === String(value);
    } else if (action === 'click') {
      if (!sel) { ok = true; effect = 'no-op'; return; }
      const loc = page.locator(sel).first();
      const count = await loc.count();
      if (!count) { effect = 'element not found'; ok = false; return; }
      await safeScrollIntoView(page, loc);
      const beforeState = await page.evaluate(() => ({
        drawerOpen: document.querySelector('#drawer')?.classList.contains('open'),
        popOpen: !document.querySelector('#artistPop')?.hidden,
        url: location.href,
        cards: document.querySelectorAll('.card').length,
        bodyLayout: document.body.dataset.layout,
        colsVisible: document.querySelector('#collagesView') && !document.querySelector('#collagesView').hidden,
      }));
      await loc.click({ timeout: 3000 });
      await page.waitForTimeout(400);
      const afterState = await page.evaluate(() => ({
        drawerOpen: document.querySelector('#drawer')?.classList.contains('open'),
        popOpen: !document.querySelector('#artistPop')?.hidden,
        url: location.href,
        cards: document.querySelectorAll('.card').length,
        bodyLayout: document.body.dataset.layout,
        colsVisible: document.querySelector('#collagesView') && !document.querySelector('#collagesView').hidden,
      }));
      effect = JSON.stringify({ before: beforeState, after: afterState });
      ok = true;
    } else if (action === 'escape') {
      if (prereq === 'openDrawerGrid') {
        await ensureDrawerOpen(page);
      }
      await page.keyboard.press('Escape');
      await page.waitForTimeout(350);
      const drawerOpen = await page.evaluate(() => document.querySelector('#drawer')?.classList.contains('open'));
      const popOpen = await page.evaluate(() => !document.querySelector('#artistPop')?.hidden);
      effect = `drawerOpen:${drawerOpen}, popOpen:${popOpen}`;
      ok = !drawerOpen && !popOpen;
    } else if (action === 'click-details') {
      await hoverAndClickFirstInfoChip(page);
      const popOpen = await page.evaluate(() => !document.querySelector('#artistPop')?.hidden);
      const popName = await page.evaluate(() => document.querySelector('.ap-name')?.textContent?.trim());
      effect = `popOpen:${popOpen}, artist:${popName}`;
      ok = popOpen;
    }
  } catch (err) {
    effect = `ERROR: ${err.message?.slice(0, 200)}`;
    capturedErrs.push(`${err.message?.slice(0, 300)}`);
    ok = false;
  }

  const newErrs = errs.splice(0);
  const allErrs = [...capturedErrs, ...newErrs];
  append({ run, ts, selector: sel, label, action, value: value !== undefined ? String(value) : undefined, ok, effect, errors: allErrs });
  console.log(`  [run${run}] ${ok ? 'OK  ' : 'FAIL'} | ${label.slice(0, 55).padEnd(55)} | ${effect.slice(0, 70)}`);
}

// ── Main: 5 runs ──
for (let run = 0; run < 5; run++) {
  console.log(`\n${'='.repeat(60)}\nRUN ${run}: ${ORDERED_ELS_RUN_DESCRIPTIONS[run]}\n${'='.repeat(60)}`);
  const runDir = `${RECDIR}/run${run}`;
  fs.mkdirSync(runDir, { recursive: true });

  const browser = await chromium.launch({ headless: true });
  const ctx = await browser.newContext({
    viewport: { width: 1600, height: 900 },
    recordVideo: { dir: runDir, size: { width: 1600, height: 900 } },
  });
  const page = await ctx.newPage();

  const errs = [];
  page.on('console', msg => {
    if (msg.type() === 'error') {
      const t = msg.text();
      if (!t.includes('favicon') && !t.includes('gtag') && !t.includes('ERR_NAME_NOT_RESOLVED')) {
        errs.push(`console.error: ${t.slice(0, 300)}`);
      }
    }
  });
  page.on('pageerror', e => errs.push(`pageerror: ${String(e).slice(0, 300)}`));
  const notFounds = [];
  page.on('response', resp => { if (resp.status() === 404) notFounds.push(`404: ${resp.url()}`); });

  // Load page
  try {
    await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 15000 });
    await waitForData(page);
    console.log(`  Page loaded OK`);
  } catch (err) {
    append({ run, ts: new Date().toISOString(), selector: null, label: 'PAGE LOAD', action: 'navigate', ok: false, effect: err.message, errors: [err.message] });
    await ctx.close(); await browser.close(); continue;
  }

  const initErrs = errs.splice(0);
  if (initErrs.length) {
    append({ run, ts: new Date().toISOString(), selector: null, label: 'PAGE LOAD ERRORS', action: 'load', ok: false, effect: 'errors on load', errors: initErrs });
    console.log(`  Load errors: ${initErrs.slice(0,2).join(' | ')}`);
  }

  // API checks
  const apiRes = await page.evaluate(async () => {
    try { const r = await fetch('/api/signatures'); const d = await r.json(); return { ok: r.ok, count: Array.isArray(d)?d.length:'n/a', status: r.status }; }
    catch(e) { return { ok: false, error: String(e) }; }
  });
  append({ run, ts: new Date().toISOString(), selector: '/api/signatures', label: 'API: /api/signatures', action: 'fetch', ok: apiRes.ok, effect: JSON.stringify(apiRes), errors: [] });
  console.log(`  /api/signatures: ${JSON.stringify(apiRes)}`);

  const evoRes = await page.evaluate(async () => {
    try { const r = await fetch('/api/signature-evolution'); const d = await r.json(); return { ok: r.ok, keys: Object.keys(d||{}).length, status: r.status }; }
    catch(e) { return { ok: false, error: String(e) }; }
  });
  append({ run, ts: new Date().toISOString(), selector: '/api/signature-evolution', label: 'API: /api/signature-evolution', action: 'fetch', ok: evoRes.ok, effect: JSON.stringify(evoRes), errors: [] });
  console.log(`  /api/signature-evolution: ${JSON.stringify(evoRes)}`);

  // ── Layout sweep (all 6 views) ──
  await page.click('#burger');
  await page.waitForTimeout(300);
  for (const [k, name] of [['gallery','Gallery'],['float','Float'],['plaque','Plaque'],['ledger','Ledger'],['vitrine','Vitrine'],['wall','Wall']]) {
    const chipSel = `#layoutChips .chip[data-l="${k}"]`;
    try {
      const chip = page.locator(chipSel).first();
      await safeScrollIntoView(page, chip);
      await chip.click({ timeout: 2000 });
      await page.waitForTimeout(250);
      const bodyLayout = await page.evaluate(() => document.body.dataset.layout);
      const cards = await page.evaluate(() => document.querySelectorAll('.card').length);
      const chipErrs = errs.splice(0);
      append({ run, ts: new Date().toISOString(), selector: chipSel, label: `Layout: ${name}`, action: 'click', ok: bodyLayout === k, effect: `body[data-layout]=${bodyLayout}, cards=${cards}`, errors: chipErrs });
      console.log(`  Layout ${name}: body[data-layout]=${bodyLayout}, cards=${cards} — ${bodyLayout===k ? 'OK' : 'FAIL'}`);
    } catch(err) {
      append({ run, ts: new Date().toISOString(), selector: chipSel, label: `Layout: ${name}`, action: 'click', ok: false, effect: `ERROR: ${err.message?.slice(0,100)}`, errors: [err.message] });
      console.log(`  Layout ${name}: ERROR ${err.message?.slice(0,60)}`);
    }
  }
  // Reset layout + close drawer
  await page.locator('#layoutChips .chip[data-l="gallery"]').first().click().catch(() => {});
  await page.waitForTimeout(200);
  await page.click('#drawerX').catch(() => {});
  await page.waitForTimeout(300);

  // ── Per-element ordered pass ──
  const ordered = orderFor(run, STATIC_ELEMENTS);
  console.log(`  Running ${ordered.length} element actions (order: ${ORDERED_ELS_RUN_DESCRIPTIONS[run]})...`);
  for (const el of ordered) {
    await doAction(page, el, errs, run);
  }

  // ── Deep popup test (beyond element list) ──
  try {
    await ensureDrawerClosed(page);
    await page.evaluate(() => { const c = document.querySelector('#catChips .chip[data-c="All"]'); if(c) c.click(); });
    await page.waitForTimeout(300);
    await hoverAndClickFirstInfoChip(page);
    await page.waitForTimeout(1200);
    const popOpen = await page.evaluate(() => !document.querySelector('#artistPop')?.hidden);
    const artistName = await page.evaluate(() => document.querySelector('.ap-name')?.textContent?.trim());
    const ctaHref = await page.evaluate(() => document.querySelector('.ap-cta a')?.getAttribute('href'));
    const sigSrc = await page.evaluate(() => document.querySelector('.ap-sig img')?.src);
    const sigNaturalW = await page.evaluate(() => document.querySelector('.ap-sig img')?.naturalWidth || 0);
    const life = await page.evaluate(() => document.querySelector('.ap-life')?.textContent?.trim());
    const hasTags = await page.evaluate(() => document.querySelectorAll('#apBody .tag').length);
    const hasBadge = await page.evaluate(() => document.querySelectorAll('#apBody .badge').length);
    const hasEvo = await page.evaluate(() => document.querySelector('.evo-strip') !== null);
    const hasMuralCta = await page.evaluate(() => !!document.querySelector('.ap-cta a'));
    const popErrs = errs.splice(0);
    append({ run, ts: new Date().toISOString(), selector: '#artistPop', label: 'Popup deep: structure check', action: 'inspect', ok: popOpen && hasMuralCta, effect: JSON.stringify({ popOpen, artistName, ctaHref, sigNaturalW, life, hasTags, hasBadge, hasEvo, hasMuralCta }), errors: popErrs });
    console.log(`  Popup: open=${popOpen}, artist=${artistName}, cta=${ctaHref}, sig-loaded=${sigNaturalW>0}, evo=${hasEvo}`);

    // Check signature image actually loads
    const sigOk = await page.evaluate(async (src) => {
      try { const r = await fetch(src); return r.ok && r.headers.get('content-type')?.startsWith('image'); } catch { return false; }
    }, sigSrc);
    append({ run, ts: new Date().toISOString(), selector: '.ap-sig img', label: 'Popup sig img: HTTP check', action: 'check-img', ok: !!sigOk, effect: `src=${sigSrc?.slice(0,80)}, ok=${sigOk}`, errors: [] });
    console.log(`  Popup sig img OK: ${sigOk}, src: ${sigSrc?.slice(0,60)}`);

    // Wait for works
    await page.waitForTimeout(4000);
    const worksCount = await page.evaluate(() => document.querySelectorAll('#apWorks a').length);
    const worksText = await page.evaluate(() => document.querySelector('#apWorks')?.textContent?.slice(0, 150).trim());
    const worksImgBroken = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('#apWorks img')).filter(i => i.naturalWidth === 0 && i.complete).length;
    });
    append({ run, ts: new Date().toISOString(), selector: '#apWorks', label: 'Popup works: loaded after 4s', action: 'check-works', ok: worksCount > 0 || (worksText||'').includes('no openly'), effect: `works:${worksCount}, brokenImgs:${worksImgBroken}, text:${worksText?.slice(0,100)}`, errors: errs.splice(0) });
    console.log(`  Works: ${worksCount} tiles, ${worksImgBroken} broken imgs, text: ${worksText?.slice(0,60)}`);

    // Wikidata link present
    const wikiHref = await page.evaluate(() => document.querySelector('#apBody a[href*="wikidata"]')?.href);
    const sourceHref = await page.evaluate(() => document.querySelector('#apBody a[href*="backup_source"], #apBody .ap-sub a:last-child')?.href);
    append({ run, ts: new Date().toISOString(), selector: '#apBody a[href*="wikidata"]', label: 'Popup: Wikidata link', action: 'check-link', ok: !!wikiHref, effect: `href:${wikiHref}`, errors: [] });
    console.log(`  Wikidata link: ${wikiHref?.slice(0,60)}`);

    // Close via ×
    await page.click('.ap-x');
    await page.waitForTimeout(300);
    const closed = await page.evaluate(() => document.querySelector('#artistPop')?.hidden);
    append({ run, ts: new Date().toISOString(), selector: '.ap-x', label: 'Popup × close (deep test)', action: 'click', ok: closed, effect: `hidden:${closed}`, errors: errs.splice(0) });
    console.log(`  Popup closed: ${closed}`);
  } catch(err) {
    append({ run, ts: new Date().toISOString(), selector: '#artistPop', label: 'Popup deep test', action: 'deep', ok: false, effect: `ERROR: ${err.message?.slice(0,200)}`, errors: [err.message] });
    console.log(`  Popup deep test ERROR: ${err.message?.slice(0,100)}`);
  }

  // ── Collages content check ──
  try {
    await ensureDrawerOpen(page);
    await page.locator('.drawer .tab[data-v="collages"]').first().click({ timeout: 2000 });
    await page.waitForTimeout(600);
    const collageCount = await page.evaluate(() => document.querySelectorAll('.collage').length);
    const collageImgs = await page.evaluate(() => document.querySelectorAll('.collage img').length);
    const collageNotes = await page.evaluate(() => document.querySelectorAll('.collage .note').length);
    const downloadLinks = await page.evaluate(() => Array.from(document.querySelectorAll('.collage a[download]')).map(a=>a.getAttribute('href')));
    // Check which collages have "not built yet" errors
    const notBuilt = await page.evaluate(() =>
      Array.from(document.querySelectorAll('.collage .c')).filter(c=>c.textContent.includes('not built yet')).map(c=>c.closest('.collage')?.querySelector('b')?.textContent)
    );
    const colErrs = errs.splice(0);
    append({ run, ts: new Date().toISOString(), selector: '#collagesView', label: 'Collages: content', action: 'check', ok: collageCount > 0, effect: JSON.stringify({ collageCount, collageImgs, collageNotes, downloadLinks, notBuilt }), errors: colErrs });
    console.log(`  Collages: ${collageCount} items, ${collageImgs} imgs, ${collageNotes} notes, ${downloadLinks.length} download links`);
    if (notBuilt.length) console.log(`  Collages not built yet: ${notBuilt.join(', ')}`);
    await page.locator('.drawer .tab[data-v="grid"]').first().click({ timeout: 2000 }).catch(() => {});
    await page.waitForTimeout(200);
    await page.click('#drawerX').catch(() => {});
    await page.waitForTimeout(200);
  } catch(err) {
    append({ run, ts: new Date().toISOString(), selector: '#collagesView', label: 'Collages content check', action: 'check', ok: false, effect: `ERROR: ${err.message?.slice(0,150)}`, errors: [err.message] });
    console.log(`  Collages ERROR: ${err.message?.slice(0,80)}`);
  }

  // ── 404s & final errors ──
  if (notFounds.length) {
    append({ run, ts: new Date().toISOString(), selector: null, label: '404 responses', action: 'network', ok: false, effect: notFounds.join(' | ').slice(0,500), errors: notFounds });
    console.log(`  404s (${notFounds.length}): ${notFounds.slice(0,3).join(' | ')}`);
  }
  const finalErrs = errs.splice(0);
  if (finalErrs.length) {
    append({ run, ts: new Date().toISOString(), selector: null, label: 'End-of-run errors', action: 'end', ok: false, effect: 'residual', errors: finalErrs });
  }

  await page.waitForTimeout(400);
  await ctx.close();
  await browser.close();

  // Convert .webm → .mp4
  const webms = fs.existsSync(runDir) ? fs.readdirSync(runDir).filter(f => f.endsWith('.webm')) : [];
  for (const w of webms) {
    const inF = `${runDir}/${w}`, outF = `${runDir}/${w.replace('.webm','.mp4')}`;
    try {
      execSync(`ffmpeg -y -i "${inF}" -c:v libx264 -preset fast -crf 22 "${outF}" 2>/dev/null`);
      fs.unlinkSync(inF);
      console.log(`  Converted ${w} → .mp4`);
    } catch(e) { console.log(`  ffmpeg failed for ${w} (leaving .webm)`); }
  }
  console.log(`  Run ${run} done.`);
}

console.log(`\nAll 5 runs complete. Log: ${LOG}`);

// ── Generate DEBUG-REPORT.md ──
const allEntries = fs.readFileSync(LOG, 'utf8').trim().split('\n').filter(Boolean)
  .map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);

// Only this session's 5 runs (runs 0-4, last batch)
// Take the last batch: find the last run=0 and go from there
let batchStart = 0;
for (let i = allEntries.length - 1; i >= 0; i--) {
  if (allEntries[i].run === 0) { batchStart = i; break; }
}
const entries = allEntries.slice(batchStart);
const errEntries = entries.filter(e => e.errors && e.errors.length > 0);
const failEntries = entries.filter(e => !e.ok);

// Group errors by selector
const bySelector = {};
for (const e of errEntries) {
  const k = e.selector || '(page-level)';
  if (!bySelector[k]) bySelector[k] = [];
  bySelector[k].push(e);
}

// Order-dependent: fail in some run, ok in another
const byKey = {};
for (const e of entries) {
  const k = `${e.selector}|||${e.label}`;
  if (!byKey[k]) byKey[k] = [];
  byKey[k].push(e);
}
const orderDependent = Object.entries(byKey).filter(([, arr]) => arr.some(e => e.ok) && arr.some(e => !e.ok) && arr.some(e => e.errors?.length > 0));

// Recordings
const recPaths = [];
for (let r = 0; r < 5; r++) {
  const rd = `${RECDIR}/run${r}`;
  if (fs.existsSync(rd)) {
    const files = fs.readdirSync(rd).filter(f => f.endsWith('.mp4') || f.endsWith('.webm'));
    recPaths.push(`Run ${r} (${ORDERED_ELS_RUN_DESCRIPTIONS[r]}): ${files.map(f=>`${rd}/${f}`).join(', ') || 'no file'}`);
  }
}

// Highest-leverage fix = selector with most errors
const topErr = Object.entries(bySelector).sort(([,a],[,b]) => b.length - a.length)[0];

const reportContent = `# Celebrity Signatures — Screen Record Debug Report
Generated: ${new Date().toISOString()}
5 runs × distinct click-order combinations
API: ${JSON.stringify((entries.find(e=>e.selector==='/api/signatures')||{}).effect)}

## HIGHEST-LEVERAGE FIX
${topErr
  ? `**\`${topErr[0]}\`** — ${topErr[1].length} error(s) across runs\n${topErr[1].map(e=>`- Run${e.run} | ${e.label}: ${e.errors.join('; ').slice(0,200)}`).join('\n')}`
  : '**No selector-level errors found.**\n\nSee page-level / 404 section for issues.'}

## Per-Run Recordings
${recPaths.join('\n')}

## Console Errors & Pageerrors (by selector)
${Object.keys(bySelector).length === 0 ? '_None_' :
  Object.entries(bySelector).map(([sel, arr]) =>
    `### \`${sel}\`\n${arr.map(e => `- Run${e.run} | **${e.label}** | ${e.errors.join(' | ').slice(0,300)}`).join('\n')}`
  ).join('\n\n')}

## Failed Actions (ok=false)
${failEntries.length === 0 ? '_None_' :
  failEntries.map(e => `- Run${e.run} | \`${e.selector||'n/a'}\` | ${e.label} | ${(e.effect||'').slice(0,150)}`).join('\n')}

## Order-Dependent Failures
_Selectors that fail in some run orderings but succeed in others:_
${orderDependent.length === 0 ? '_None detected_' :
  orderDependent.map(([key, arr]) => {
    const [sel, lab] = key.split('|||');
    return `### ${lab} (\`${sel}\`)\n${arr.map(e=>`- Run${e.run}: ok=${e.ok}${e.errors?.length?', ERRORS: '+e.errors.join('; ').slice(0,200):''}`).join('\n')}`;
  }).join('\n\n')}

## Network 404s
${entries.filter(e=>e.label==='404 responses').map(e=>`- Run${e.run}: ${e.effect.slice(0,400)}`).join('\n') || '_None_'}

## Summary
| Metric | Value |
|---|---|
| Total actions | ${entries.length} |
| OK | ${entries.filter(e=>e.ok).length} |
| Failed | ${failEntries.length} |
| With errors | ${errEntries.length} |
| Selectors with errors | ${Object.keys(bySelector).length} |
| Order-dependent | ${orderDependent.length} |
`;

fs.writeFileSync(`${ROOT}/screenrecord/DEBUG-REPORT.md`, reportContent);
console.log(`DEBUG-REPORT.md written to ${ROOT}/screenrecord/DEBUG-REPORT.md`);