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