import { spawn } from 'node:child_process'; import { writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; const CHROME_PATH = 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'; const USER_DATA = join(process.cwd(), '.chrome-test-profile'); const SCREENSHOT_DIR = join(process.cwd(), 'test-support', 'visual-evidence'); try { mkdirSync(SCREENSHOT_DIR, { recursive: true }); } catch {} async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function run() { console.log('[Browser Verification] Starting Chrome in headless mode...'); const chrome = spawn( CHROME_PATH, [ '--headless=new', '--remote-debugging-port=9222', '--disable-gpu', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${USER_DATA}`, '--window-size=1280,960', 'about:blank', ], { stdio: 'ignore' } ); try { // Wait for Chrome remote debugging endpoint let versionData = null; for (let i = 0; i < 30; i++) { try { const res = await fetch('http://127.0.0.1:9222/json/version'); if (res.ok) { versionData = await res.json(); break; } } catch {} await sleep(200); } if (!versionData) { throw new Error('Failed to connect to Chrome DevTools endpoint on port 9222'); } console.log('[Browser Verification] Connected to Chrome DevTools:', versionData.Browser); // Create a new target page const testUrl = process.env.REPLAY_TEST_URL || process.argv[2] || 'http://localhost:3008/account'; const newTargetRes = await fetch(`http://127.0.0.1:9222/json/new?${encodeURIComponent(testUrl)}`, { method: 'PUT', }); const target = await newTargetRes.json(); console.log('[Browser Verification] Opened target page:', target.id); const ws = new WebSocket(target.webSocketDebuggerUrl); let messageId = 1; const callbacks = new Map(); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.id && callbacks.has(msg.id)) { const cb = callbacks.get(msg.id); callbacks.delete(msg.id); if (msg.error) { cb.reject(new Error(msg.error.message)); } else { cb.resolve(msg.result); } } }; await new Promise((resolve) => { ws.onopen = resolve; }); const send = (method, params = {}) => { return new Promise((resolve, reject) => { const id = messageId++; callbacks.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); }); }; await send('Page.enable'); await send('Runtime.enable'); console.log('[Browser Verification] Waiting for page load and Leaflet map rendering...'); await sleep(2500); const evalJs = async (expr) => { const res = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true, }); return res?.result?.value; }; // 1. Verify modal title and elements const title = await evalJs('document.querySelector("h2")?.textContent'); console.log('[Browser Verification] Modal Title:', title); const markerExists = await evalJs('!!document.querySelector(".rider-replay-marker")'); console.log('[Browser Verification] Rider marker in DOM:', markerExists); const initialPos = await evalJs(`(() => { const el = document.querySelector(".rider-replay-marker"); const arrow = document.querySelector(".rider-heading-arrow"); return { markerTransform: el ? el.style.transform : null, arrowTransform: arrow ? arrow.style.transform : null, }; })()`); console.log('[Browser Verification] Initial Marker State:', initialPos); // Capture screenshot: initial const snap1 = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(SCREENSHOT_DIR, '01_initial_replay.png'), Buffer.from(snap1.data, 'base64')); console.log('[Browser Verification] Saved screenshot: 01_initial_replay.png'); // 2. Click Play console.log('[Browser Verification] Clicking PLAY...'); await evalJs('document.querySelector(\'[data-testid="play-pause-btn"]\')?.click()'); await sleep(100); // Sample marker positions over 300 ms (approx 18 animation frames) console.log('[Browser Verification] Sampling marker positions in motion across animation frames...'); const samples = []; for (let i = 0; i < 6; i++) { await sleep(50); const sample = await evalJs(`(() => { const el = document.querySelector(".rider-replay-marker"); const arrow = document.querySelector(".rider-heading-arrow"); const speed = document.querySelector(".text-3xl.font-extrabold")?.textContent; return { markerTransform: el ? el.style.transform : null, arrowTransform: arrow ? arrow.style.transform : null, speed, }; })()`); samples.push(sample); } console.log('[Browser Verification] Recorded motion frames:'); samples.forEach((s, idx) => console.log(` Frame sample ${idx + 1}: ${s.markerTransform} | heading: ${s.arrowTransform} | speed: ${s.speed} mph`)); // Verify intermediate sub-pixel movement occurred const uniqueTransforms = new Set(samples.map((s) => s.markerTransform)); console.log(`[Browser Verification] Unique positions observed in 300ms: ${uniqueTransforms.size} / ${samples.length}`); if (uniqueTransforms.size > 1) { console.log('>>> SUCCESS: Marker smoothly animated across intermediate screen positions! <<<'); } else { console.error('>>> FAIL: Marker did not move! <<<'); } // Capture screenshot: in motion const snap2 = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(SCREENSHOT_DIR, '02_motion_replay.png'), Buffer.from(snap2.data, 'base64')); console.log('[Browser Verification] Saved screenshot: 02_motion_replay.png'); // 3. Click Pause console.log('[Browser Verification] Clicking PAUSE...'); await evalJs('document.querySelector(\'[data-testid="play-pause-btn"]\')?.click()'); const pausedState1 = await evalJs('document.querySelector(".rider-replay-marker")?.style.transform'); console.log('[Browser Verification] Position immediately upon pause:', pausedState1); await sleep(500); const pausedState2 = await evalJs('document.querySelector(".rider-replay-marker")?.style.transform'); console.log('[Browser Verification] Position 500ms after pause (checking drift):', pausedState2); if (pausedState1 === pausedState2) { console.log('>>> SUCCESS: Zero marker drift on pause! <<<'); } else { console.error('>>> FAIL: Marker drifted while paused! <<<'); } // Capture screenshot: paused const snap3 = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(SCREENSHOT_DIR, '03_paused_replay.png'), Buffer.from(snap3.data, 'base64')); console.log('[Browser Verification] Saved screenshot: 03_paused_replay.png'); // 4. Test Speed change to 2x and Resume console.log('[Browser Verification] Testing speed change to 2x and resume...'); await evalJs(`(() => { const btn = Array.from(document.querySelectorAll("button")).find(b => b.textContent.trim() === "2x"); btn?.click(); })()`); await sleep(100); await evalJs('document.querySelector(\'[data-testid="play-pause-btn"]\')?.click()'); await sleep(300); const speedSample = await evalJs('document.querySelector(".rider-replay-marker")?.style.transform'); console.log('[Browser Verification] Position after 2x speed playback:', speedSample); // 5. Test Seek to exact point 100 console.log('[Browser Verification] Testing timeline seek...'); await evalJs(`(() => { const slider = document.querySelector('[data-testid="replay-timeline-slider"]'); if (slider) { slider.value = '100'; slider.dispatchEvent(new Event('change', { bubbles: true })); } })()`); await sleep(150); const seekPos = await evalJs(`(() => { const el = document.querySelector(".rider-replay-marker"); const arrow = document.querySelector(".rider-heading-arrow"); return { markerTransform: el ? el.style.transform : null, arrowTransform: arrow ? arrow.style.transform : null, }; })()`); console.log('[Browser Verification] Position after seek to point 100:', seekPos); // Capture screenshot: seek const snap4 = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(join(SCREENSHOT_DIR, '04_seek_replay.png'), Buffer.from(snap4.data, 'base64')); console.log('[Browser Verification] Saved screenshot: 04_seek_replay.png'); ws.close(); } finally { chrome.kill('SIGKILL'); console.log('[Browser Verification] Chrome process terminated.'); } } run().catch((err) => { console.error('[Browser Verification Error]:', err); process.exit(1); });