239 lines
8.9 KiB
JavaScript
239 lines
8.9 KiB
JavaScript
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);
|
|
});
|