← back to Dead Agentabrams
scripts/screenrecord-5pass.js
566 lines
'use strict';
// 5-pass screen recording + click-every-element debug agent
// dead.agentabrams.com/room/ — Grateful Dead music room
// NODE_PATH=$HOME/.npm-global/lib/node_modules node scripts/screenrecord-5pass.js
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const URL = 'https://dead.agentabrams.com/room/';
const CHROME_EXE = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const LOG = '/Users/macstudio3/Projects/dead-agentabrams/screenrecord/debug-log.jsonl';
const REC_BASE = '/Users/macstudio3/Projects/dead-agentabrams/screenrecord/rec';
fs.mkdirSync(path.dirname(LOG), { recursive: true });
for (let r = 0; r < 5; r++) fs.mkdirSync(path.join(REC_BASE, `run${r}`), { recursive: true });
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 = new Set(prior.filter(r => r.errors && r.errors.length > 0).map(r => r.selector));
console.log(`Prior log entries: ${prior.length}, prior errored selectors: ${priorErrors.size}`);
function append(obj) {
fs.appendFileSync(LOG, JSON.stringify(obj) + '\n');
}
function seededShuffle(arr, seed) {
const s = [...arr];
let state = seed;
for (let i = s.length - 1; i > 0; i--) {
state = (state * 1664525 + 1013904223) & 0xffffffff;
const j = Math.abs(state) % (i + 1);
[s[i], s[j]] = [s[j], s[i]];
}
return s;
}
function orderFor(run, els) {
switch(run) {
case 0: return [...els];
case 1: return [...els].reverse();
case 2: return [...els].sort((a, b) => (b.type === 'range' ? 1 : 0) - (a.type === 'range' ? 1 : 0));
case 3: return seededShuffle(els, 42 * 13 + run * 7);
case 4: {
const bad = sel => priorErrors.has(sel) ? 0 : 1;
return [...els].sort((a, b) => bad(a.selector) - bad(b.selector));
}
}
}
async function runPass(runIdx) {
console.log(`\n===== RUN ${runIdx} =====`);
const recDir = path.join(REC_BASE, `run${runIdx}`);
const browser = await chromium.launch({
executablePath: CHROME_EXE,
headless: true,
args: ['--no-sandbox']
});
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
recordVideo: { dir: recDir, size: { width: 1600, height: 900 } }
});
const page = await ctx.newPage();
const consoleErrors = [];
const pageErrors = [];
const archiveRequests = [];
const archiveErrors = [];
page.on('console', m => {
const txt = m.text().slice(0, 300);
if (m.type() === 'error') consoleErrors.push(txt);
if (txt.includes('archive') || txt.includes('fetch') || txt.includes('Error')) {
consoleErrors.push(`[console:${m.type()}] ${txt}`);
}
});
page.on('pageerror', e => pageErrors.push(('' + e).slice(0, 300)));
page.on('requestfailed', req => {
if (req.url().includes('archive.org')) {
archiveErrors.push({ url: req.url().slice(0, 120), err: req.failure()?.errorText });
}
});
page.on('response', resp => {
if (resp.url().includes('archive.org')) {
archiveRequests.push({ url: resp.url().slice(0, 120), status: resp.status() });
}
});
const loadStart = Date.now();
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
const domReadyMs = Date.now() - loadStart;
await page.waitForTimeout(2000);
// Capture bar heights at load (BEFORE archive.org fills them)
const barsAtLoad = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
return {
count: bars.length,
sampleStyles: bars.slice(0, 3).map(b => ({ style: b.style.height, computed: window.getComputedStyle(b).height }))
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: 'PAGE_LOAD', label: 'page load',
action: 'navigate', ok: true,
effect: `domReady=${domReadyMs}ms barsAtLoad=${JSON.stringify(barsAtLoad)}`,
errors: [...consoleErrors.splice(0), ...pageErrors.splice(0)] });
// ---- Step 1: Open hamburger ----
let hamburgerOk = false;
try {
await page.click('#chromeBtn', { force: true, timeout: 5000 });
await page.waitForTimeout(800);
hamburgerOk = true;
} catch(e) {
append({ run: runIdx, ts: new Date().toISOString(), selector: '#chromeBtn', label: 'hamburger',
action: 'click', ok: false, effect: 'FAILED: ' + e.message.slice(0,100), errors: [] });
}
append({ run: runIdx, ts: new Date().toISOString(), selector: '#chromeBtn', label: 'hamburger ☰ Controls',
action: 'click', ok: hamburgerOk, effect: `panel_revealed=${hamburgerOk}`,
errors: [...consoleErrors.splice(0)] });
await page.screenshot({ path: path.join(recDir, '01-after-hamburger.png') });
// ---- Discover all visible elements after panel open ----
// IMPORTANT: We do NOT include #mrToggle in click list (it would collapse the music room section)
const rawEls = await page.evaluate(() => {
const result = [];
const seen = new Set();
document.querySelectorAll('button, [role=button], input, select, a[href]').forEach(el => {
const id = el.id;
const cls = el.className.toString().trim();
const key = id ? `#${id}` : (cls ? `${el.tagName.toLowerCase()}.${cls.split(/\s+/).join('.')}` : null);
if (!key || seen.has(key)) return;
seen.add(key);
const rect = el.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return; // skip invisible
result.push({
selector: id ? `#${id}` : `${el.tagName.toLowerCase()}.${cls.split(/\s+/)[0]}`,
label: el.textContent?.trim().slice(0, 50) || el.id || el.placeholder || 'unlabeled',
type: el.tagName === 'INPUT' ? el.type : (el.tagName === 'SELECT' ? 'select' : 'button'),
rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }
});
});
return result;
});
// Filter out navigation links (would leave page), and mrToggle (would collapse music room section)
const els = rawEls.filter(e =>
e.selector !== '#backlink' &&
e.selector !== '#mrToggle' &&
e.selector !== '#chromeBtn' // already clicked
);
console.log(` Run ${runIdx}: ${els.length} elements to interact with`);
// ---- Year bar measurements BEFORE clicking ----
const yearBarMeasure = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
const containerEl = bars[0]?.parentElement;
if (!containerEl) return { found: false };
const cRect = containerEl.getBoundingClientRect();
const barRects = bars.map(b => {
const r = b.getBoundingClientRect();
return { text: b.textContent?.trim(), x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
return {
found: true,
containerW: Math.round(cRect.width),
containerScrollW: containerEl.scrollWidth,
overflows: containerEl.scrollWidth > cRect.width + 2,
barCount: bars.length,
firstBar: barRects[0],
lastBar: barRects[barRects.length - 1],
allBarsW11: barRects.every(b => b.w === 11),
narrowBars: barRects.filter(b => b.w < 20).length,
sampleRects: barRects.slice(0, 3)
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: 'YEAR_MAP_MEASURE',
label: 'year bar measurements', action: 'measure', ok: true,
effect: JSON.stringify(yearBarMeasure), errors: [] });
console.log(` Year bars: count=${yearBarMeasure.barCount} overflow=${yearBarMeasure.overflows} narrow=${yearBarMeasure.narrowBars} first=${JSON.stringify(yearBarMeasure.firstBar)}`);
// ---- "All years" button size ----
const allBtnMeasure = await page.evaluate(() => {
const btn = document.querySelector('#mrAll');
if (!btn) return { found: false };
const r = btn.getBoundingClientRect();
return { found: true, w: Math.round(r.width), h: Math.round(r.height), x: Math.round(r.x), y: Math.round(r.y), text: btn.textContent?.trim() };
});
append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrAll:measure',
label: '"All years" button size', action: 'measure', ok: true,
effect: JSON.stringify(allBtnMeasure), errors: [] });
// ---- Seek slider height ----
const seekMeasure = await page.evaluate(() => {
const el = document.querySelector('#mrSeek');
if (!el) return { found: false };
const r = el.getBoundingClientRect();
return { found: true, w: Math.round(r.width), h: Math.round(r.height) };
});
append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrSeek:measure',
label: 'seek slider dimensions', action: 'measure', ok: true,
effect: JSON.stringify(seekMeasure), errors: [] });
// ---- Header / label visibility check ----
const headerCheck = await page.evaluate(() => {
const header = document.querySelector('.mr-header, [class*=header]');
const label = document.querySelector('[aria-label*="Touring"], [aria-label*="year"], h2, h3');
const allEl = document.querySelector('#mrAll');
const searchEl = document.querySelector('#mrSearch');
return {
hasHeader: !!header,
headerText: header?.textContent?.trim().slice(0, 80),
hasLabel: !!label,
labelText: label?.textContent?.trim().slice(0, 80),
allBtnFound: !!allEl,
searchFound: !!searchEl,
bodyHTML_sample: document.querySelector('.musicroom')?.innerHTML?.slice(0, 600)
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: 'HEADER_CHECK',
label: 'header/label visibility', action: 'measure', ok: true,
effect: JSON.stringify(headerCheck), errors: [] });
// ---- Apply ordering for this run ----
const ordered = orderFor(runIdx, els);
// ---- Click/interact every element ----
for (const el of ordered) {
const errsBefore = consoleErrors.length;
let ok = false;
let effect = '';
let action = 'click';
let errMsg = '';
// Snapshot before for key elements
let beforeState = '';
if (el.selector === '#mrAll') {
beforeState = await page.evaluate(() => {
const b = document.querySelector('#mrAll');
return b ? `cls=${b.className} text=${b.textContent?.trim()}` : '';
});
}
try {
if (el.type === 'range') {
action = 'drag';
// Click at center, then at 80% position using coordinates
const cx = el.rect.x + el.rect.w * 0.5;
const cy = el.rect.y + el.rect.h * 0.5;
const r8 = el.rect.x + el.rect.w * 0.8;
await page.mouse.click(cx, cy);
await page.waitForTimeout(100);
await page.mouse.click(r8, cy);
await page.waitForTimeout(100);
const val = await page.evaluate(sel => { const e = document.querySelector(sel); return e?.value; }, el.selector);
effect = `dragged box=${el.rect.w}x${el.rect.h} val=${val}`;
ok = true;
} else if (el.type === 'search' || el.selector === '#mrSearch') {
action = 'type';
await page.mouse.click(el.rect.x + el.rect.w * 0.5, el.rect.y + el.rect.h * 0.5);
await page.waitForTimeout(200);
await page.keyboard.type('1972');
await page.waitForTimeout(600);
const srchResult = await page.evaluate(() => {
const sel = document.querySelector('#mrShow');
return sel ? `${Array.from(sel.options).length} options visible` : 'select not found';
});
effect = `typed '1972', results: ${srchResult}`;
// Clear
await page.keyboard.selectAll();
await page.keyboard.press('Backspace');
await page.waitForTimeout(300);
ok = true;
} else if (el.type === 'select') {
action = 'select';
const optCount = await page.evaluate(sel => {
const s = document.querySelector(sel);
return s ? Array.from(s.options).length : 0;
}, el.selector);
effect = `${optCount} options`;
if (optCount > 1) {
// Pick index 1 (second item)
await page.selectOption(el.selector, { index: 1 });
await page.waitForTimeout(500);
}
ok = true;
} else {
// Click by coordinates (bypasses scrollIntoView stability checks)
const cx = el.rect.x + el.rect.w * 0.5;
const cy = el.rect.y + el.rect.h * 0.5;
if (cx < 0 || cy < 0 || cx > 1600 || cy > 900) {
effect = `out of viewport at (${cx},${cy}) -- skipped`;
ok = false;
} else {
await page.mouse.click(cx, cy);
await page.waitForTimeout(350);
effect = `clicked at (${Math.round(cx)},${Math.round(cy)}) box=${el.rect.w}x${el.rect.h}`;
ok = true;
}
}
} catch(e) {
errMsg = e.message.slice(0, 200);
effect = `ERROR: ${errMsg}`;
}
// Capture after-state
let afterState = '';
if (el.selector === '#mrAll') {
afterState = await page.evaluate(() => {
const b = document.querySelector('#mrAll');
const yi = document.querySelector('#mrYear');
return b ? `cls=${b.className} text=${b.textContent?.trim()} yearVal=${yi?.value}` : '';
});
} else if (el.selector && el.selector.match(/mrPlay|playBtn/)) {
afterState = await page.evaluate(() => {
const mp = document.querySelector('#mrPlay');
return mp ? `mrPlay=${mp.textContent?.trim()}` : '';
});
}
const newErrs = consoleErrors.slice(errsBefore);
const newPageErrs = [...pageErrors.splice(0)];
const allNewErrs = [...newErrs, ...newPageErrs, ...(errMsg ? [errMsg] : [])];
const logEntry = {
run: runIdx,
ts: new Date().toISOString(),
selector: el.selector,
label: el.label,
action,
ok,
effect: [effect, afterState ? `after:${afterState}` : '', beforeState ? `before:${beforeState}` : ''].filter(Boolean).join(' | '),
errors: allNewErrs
};
if (allNewErrs.length) {
console.log(` [R${runIdx}] ERR ${el.label.slice(0,30)}: ${allNewErrs[0].slice(0,80)}`);
} else {
console.log(` [R${runIdx}] OK ${el.label.slice(0,30)}: ${effect.slice(0, 60)}`);
}
append(logEntry);
// After clicking a year bar, record state
if (el.selector && (el.selector.includes('mr-yr') || el.label.match(/196[0-9]|197[0-9]|198[0-9]|199[0-9]/))) {
const yrState = await page.evaluate(() => {
const allBtn = document.querySelector('#mrAll');
const bars = Array.from(document.querySelectorAll('.mr-yr'));
const active = bars.filter(b => b.classList.contains('active') || b.getAttribute('aria-pressed') === 'true');
return {
allBtnCls: allBtn?.className,
activeCount: active.length,
activeTexts: active.map(b => b.textContent?.trim())
};
});
append({ run: runIdx, ts: new Date().toISOString(),
selector: el.selector + ':yrstate', label: 'year state snapshot',
action: 'snapshot', ok: true, effect: JSON.stringify(yrState), errors: [] });
}
}
// ---- Screenshot mid-run ----
await page.screenshot({ path: path.join(recDir, '02-mid-run.png') });
// ---- Wait for archive.org bar height animation ----
console.log(` Waiting 6s for archive.org bar heights to load...`);
const waitStart = Date.now();
await page.waitForTimeout(6000);
const barsAfterWait = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
const barSpans = Array.from(document.querySelectorAll('.mr-yr .bar, .mr-yr > span'));
return {
barCount: bars.length,
sampleParentStyles: bars.slice(0, 5).map(b => ({ style: b.style.height, computed: window.getComputedStyle(b).height })),
spanCount: barSpans.length,
sampleSpanHeights: barSpans.slice(0, 5).map(s => window.getComputedStyle(s).height),
hasRealHeights: bars.some(b => b.style.height && b.style.height !== '0px' && b.style.height !== 'auto'),
archiveDataAttr: bars.slice(0,3).map(b => ({
dataCount: b.getAttribute('data-count'),
dataYear: b.textContent?.trim()
}))
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: 'BAR_HEIGHT_TIMING',
label: `bar heights after ${Math.round((Date.now() - waitStart + 6000) / 1000)}s wait`,
action: 'measure', ok: true, effect: JSON.stringify(barsAfterWait),
errors: archiveErrors.map(e => `archive FAIL: ${e.url} ${e.err}`) });
console.log(` Archive reqs: ${archiveRequests.length} ok, ${archiveErrors.length} fail`);
console.log(` Bars have real heights: ${barsAfterWait.hasRealHeights}`);
// ---- Click a specific year bar to test selection feedback ----
const bar1980 = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
const b = bars.find(b => b.textContent?.trim() === '1980') || bars[15];
if (!b) return null;
const r = b.getBoundingClientRect();
return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
if (bar1980) {
const cx = bar1980.x + bar1980.w * 0.5;
const cy = bar1980.y + bar1980.h * 0.5;
console.log(` Clicking 1980 bar at (${cx},${cy}) size ${bar1980.w}x${bar1980.h}`);
// Take screenshot before click
await page.screenshot({ path: path.join(recDir, '03-before-year-click.png') });
await page.mouse.click(cx, cy);
await page.waitForTimeout(1000);
// Check selection feedback
const selectionFeedback = await page.evaluate(() => {
const bars = Array.from(document.querySelectorAll('.mr-yr'));
const active = bars.filter(b => b.classList.contains('active') || b.getAttribute('aria-pressed') === 'true' || b.style.opacity === '1' || b.style.background);
const resultList = document.querySelector('#mrList, [id*=List], [class*=result]');
const header = document.querySelector('.mr-header, h3, .section-label');
return {
activeBarCount: active.length,
activeBarTexts: active.map(b => b.textContent?.trim()),
resultItems: resultList ? resultList.querySelectorAll('option, li, .item').length : 0,
hasVisualFeedback: active.length > 0,
headerText: header?.textContent?.trim()
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: '.mr-yr[1980]',
label: 'click 1980 bar — selection feedback', action: 'click', ok: true,
effect: JSON.stringify(selectionFeedback), errors: [...consoleErrors.splice(0)] });
console.log(` Selection feedback: ${JSON.stringify(selectionFeedback)}`);
await page.screenshot({ path: path.join(recDir, '04-after-year-click.png') });
// Click "All years" to reset
const allRect = await page.evaluate(() => {
const b = document.querySelector('#mrAll');
if (!b) return null;
const r = b.getBoundingClientRect();
return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
});
if (allRect) {
await page.mouse.click(allRect.x + allRect.w * 0.5, allRect.y + allRect.h * 0.5);
await page.waitForTimeout(500);
const afterReset = await page.evaluate(() => {
const b = document.querySelector('#mrAll');
const yi = document.querySelector('#mrYear');
return { allBtnCls: b?.className, yearVal: yi?.value };
});
append({ run: runIdx, ts: new Date().toISOString(), selector: '#mrAll:reset',
label: 'All years reset after 1980 click', action: 'click', ok: true,
effect: JSON.stringify(afterReset), errors: [] });
}
}
// ---- State leak check: reload and measure ----
// (done only on run 2 to check if selected year persists across reload)
if (runIdx === 2) {
const stateBeforeReload = await page.evaluate(() => {
return {
mrYearVal: document.querySelector('#mrYear')?.value,
allBtnCls: document.querySelector('#mrAll')?.className,
localStorageKeys: Object.keys(localStorage)
};
});
await page.reload({ waitUntil: 'domcontentloaded', timeout: 20000 });
await page.waitForTimeout(2000);
// Reopen panel
await page.click('#chromeBtn', { force: true, timeout: 5000 }).catch(() => {});
await page.waitForTimeout(500);
const stateAfterReload = await page.evaluate(() => {
return {
mrYearVal: document.querySelector('#mrYear')?.value,
allBtnCls: document.querySelector('#mrAll')?.className,
localStorageKeys: Object.keys(localStorage)
};
});
append({ run: runIdx, ts: new Date().toISOString(), selector: 'STATE_LEAK_CHECK',
label: 'state leak: reload test', action: 'reload', ok: true,
effect: `before=${JSON.stringify(stateBeforeReload)} after=${JSON.stringify(stateAfterReload)}`,
errors: [] });
console.log(` State leak check: before=${JSON.stringify(stateBeforeReload)} after=${JSON.stringify(stateAfterReload)}`);
}
// ---- Final screenshot ----
await page.screenshot({ path: path.join(recDir, '05-final.png') });
await ctx.close();
await browser.close();
// Convert webm -> mp4
const recFiles = fs.readdirSync(recDir).filter(f => f.endsWith('.webm'));
const mp4Files = [];
for (const webm of recFiles) {
const webmPath = path.join(recDir, webm);
const mp4Path = webmPath.replace('.webm', '.mp4');
try {
execSync(`ffmpeg -y -i "${webmPath}" -c:v libx264 -preset fast -crf 28 "${mp4Path}" 2>/dev/null`, { timeout: 90000 });
mp4Files.push(mp4Path);
console.log(` Converted: ${path.basename(mp4Path)}`);
} catch(e) {
console.log(` ffmpeg failed: ${e.message.slice(0,80)}`);
mp4Files.push(webmPath); // keep webm reference
}
}
return {
runIdx, recDir,
recordings: mp4Files,
archiveRequests: archiveRequests.length,
archiveErrors: archiveErrors.length,
archiveStatus: barsAfterWait,
yearBarMeasure,
allBtnMeasure,
seekMeasure,
headerCheck
};
}
(async () => {
const results = [];
for (let r = 0; r < 5; r++) {
const res = await runPass(r);
results.push(res);
if (r < 4) await new Promise(resolve => setTimeout(resolve, 1500));
}
console.log('\n===== ALL 5 RUNS COMPLETE =====');
for (const res of results) {
console.log(`Run ${res.runIdx}: archive=${res.archiveRequests} reqs/${res.archiveErrors} errs recs=${res.recordings.map(p => path.basename(p))}`);
}
fs.writeFileSync(
'/Users/macstudio3/Projects/dead-agentabrams/screenrecord/run-results.json',
JSON.stringify(results, null, 2)
);
console.log('\nLog: ' + LOG);
console.log('Results: /Users/macstudio3/Projects/dead-agentabrams/screenrecord/run-results.json');
})().catch(e => {
console.error('FATAL:', e.message, e.stack?.slice(0, 400));
process.exit(1);
});