/** * scripts/verify-controlbar-browser.ts * * Headless Chrome browser verification of ControlBar XP bar rendering & hover interactions: * 1. Offscreen HTML Canvas Pixel Audit: * - Real OffscreenCanvas in Google Chrome. * - Renders 800CtrlPnl7 panel + ControlBarHud. * - Extracts real pixel data via getImageData(). * - Verifies groove [256, 560, 120, 4] colors. * - Verifies gold trim borders at y=559 and y=564 have ZERO pixel bleed. * 2. Real DOM Mouse Event Projection across DPRs & Viewports: * - Viewports: 800x600, 1920x1080. * - DPRs: 1.0, 1.25, 1.5, 2.0. * - Inspects window.__d2webHudInstance.controlBar.hoveredElement. */ import { spawn } from 'node:child_process' import { createServer } from 'vite' const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) async function main(): Promise { console.log('=== Headless Chrome ControlBar Stress Test ===') // 1. Start Vite dev server const server = await createServer({ root: process.cwd(), server: { port: 0, host: '127.0.0.1' }, }) await server.listen() const address = server.httpServer?.address() const port = typeof address === 'object' && address ? address.port : 5173 const baseUrl = `http://127.0.0.1:${port}` console.log(`1. Vite server running at ${baseUrl}`) // 2. Launch headless Chrome with CDP const debugPort = 9248 const chromeProc = spawn('/usr/bin/google-chrome', [ '--headless=new', `--remote-debugging-port=${debugPort}`, '--no-sandbox', '--disable-dev-shm-usage', '--enable-webgl', '--window-size=1920,1080', `${baseUrl}/acts.html?act=1`, ]) try { let wsUrl: string | null = null for (let i = 0; i < 60; i++) { await sleep(300) try { const res = await fetch(`http://127.0.0.1:${debugPort}/json/list`) const pages = (await res.json()) as { type: string; url: string; webSocketDebuggerUrl?: string }[] const page = pages.find(p => p.type === 'page' && p.url.includes('acts.html')) if (page?.webSocketDebuggerUrl) { wsUrl = page.webSocketDebuggerUrl break } } catch {} } if (!wsUrl) throw new Error('Failed to connect to Chrome CDP') console.log('2. Connected to Chrome CDP:', wsUrl) const ws = new WebSocket(wsUrl) await new Promise(resolve => { ws.onopen = () => resolve() }) let idCounter = 1 const sendCommand = (method: string, params: Record = {}): Promise => new Promise((resolve, reject) => { const id = idCounter++ const handler = (event: MessageEvent) => { const msg = JSON.parse(String(event.data)) if (msg.id === id) { ws.removeEventListener('message', handler) if (msg.error) reject(new Error(JSON.stringify(msg.error))) else resolve(msg.result as T) } } ws.addEventListener('message', handler) ws.send(JSON.stringify({ id, method, params })) }) const evalJs = async (expression: string): Promise => { const res = await sendCommand<{ result: { value: T }; exceptionDetails?: any }>('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true, }) if (res.exceptionDetails) { throw new Error(`JS exception: ${JSON.stringify(res.exceptionDetails)}`) } return res.result.value } await sendCommand('Runtime.enable') await sendCommand('Page.enable') // Wait for HudManager to be ready for (let i = 0; i < 100; i++) { const ready = await evalJs(`Boolean(window.__d2webHud?.ready && window.__d2webHudInstance)`) if (ready) break await sleep(300) } console.log('3. HudManager ready in Chrome!') // Test 1: Real OffscreenCanvas rendering & pixel audit inside Chrome console.log('\n--- Test 1: Chrome OffscreenCanvas Pixel Audit ---') const pixelAuditResult = await evalJs<{ bleedTop559Count: number bleedBot564Count: number grooveBackgroundRgb: number[] grooveFilledRgb: number[] tickNotchRgb: number[] tickPositions: number[] }>(`(() => { const hudInst = window.__d2webHudInstance; const ctrlBar = hudInst.controlBar; const ctrlImg = hudInst.images.get('ctrlPnl'); // Create base OffscreenCanvas with only background image const baseCanvas = new OffscreenCanvas(800, 600); const baseCtx = baseCanvas.getContext('2d'); baseCtx.drawImage(ctrlImg, 0, 496); const baseImgData = baseCtx.getImageData(0, 0, 800, 600).data; // Create test OffscreenCanvas and render ControlBar with 50% XP const testCanvas = new OffscreenCanvas(800, 600); const testCtx = testCanvas.getContext('2d'); const testState = { level: 1, xp: 1500, currentLevelXp: 1000, nextLevelXp: 2000, stamina: 100, maxStamina: 100, isRunning: false, unspentStatPoints: 0, unspentSkillPoints: 0, }; const assetBundle = { ctrlPnlImg: hudInst.images.get('ctrlPnl') ?? null, runBtnImg: hudInst.images.get('runbutton') ?? null, menuBtnImg: hudInst.images.get('menubutton') ?? null, miniPanelImg: hudInst.images.get('minipanel') ?? null, miniPanelBtnsImg: hudInst.images.get('minipanelBtns') ?? null, levelBtnImg: hudInst.images.get('levelBtn') ?? null, levelSocketImg: hudInst.images.get('levelSocket') ?? null, }; ctrlBar.draw(testCtx, testState, assetBundle, hudInst.font); const testImgData = testCtx.getImageData(0, 0, 800, 600).data; let bleedTop559Count = 0; let bleedBot564Count = 0; // Check row y=559 for x in [256..375] for (let x = 256; x <= 375; x++) { const idx = (559 * 800 + x) * 4; if ( testImgData[idx] !== baseImgData[idx] || testImgData[idx + 1] !== baseImgData[idx + 1] || testImgData[idx + 2] !== baseImgData[idx + 2] ) { bleedTop559Count++; } } // Check row y=564 for x in [256..375] for (let x = 256; x <= 375; x++) { const idx = (564 * 800 + x) * 4; if ( testImgData[idx] !== baseImgData[idx] || testImgData[idx + 1] !== baseImgData[idx + 1] || testImgData[idx + 2] !== baseImgData[idx + 2] ) { bleedBot564Count++; } } // Sample groove pixels // Filled area (x=260, y=561) const filledIdx = (561 * 800 + 260) * 4; const grooveFilledRgb = [testImgData[filledIdx], testImgData[filledIdx + 1], testImgData[filledIdx + 2]]; // Unfilled area (x=350, y=561) const unfilledIdx = (561 * 800 + 350) * 4; const grooveBackgroundRgb = [testImgData[unfilledIdx], testImgData[unfilledIdx + 1], testImgData[unfilledIdx + 2]]; // Tick notch at x=268, y=561 const notchIdx = (561 * 800 + 268) * 4; const tickNotchRgb = [testImgData[notchIdx], testImgData[notchIdx + 1], testImgData[notchIdx + 2]]; // Verify all 9 ticks const tickPositions = []; for (let i = 1; i < 10; i++) { const tx = 256 + i * 12; const tidx = (561 * 800 + tx) * 4; tickPositions.push(testImgData[tidx]); } return { bleedTop559Count, bleedBot564Count, grooveBackgroundRgb, grooveFilledRgb, tickNotchRgb, tickPositions, }; })()`) console.log('Pixel audit result from Chrome:', JSON.stringify(pixelAuditResult, null, 2)) // Test 2: Hover Hit-Testing in real Chrome across DPRs & Resolutions console.log('\n--- Test 2: Hover Hit-Testing in Chrome across DPRs & Resolutions ---') const resolutions = [ { w: 800, h: 600 }, { w: 1920, h: 1080 }, ] const dprs = [1.0, 1.25, 1.5, 2.0] for (const res of resolutions) { for (const dpr of dprs) { // Set emulation device metrics await sendCommand('Emulation.setDeviceMetricsOverride', { width: res.w, height: res.h, deviceScaleFactor: dpr, mobile: false, }) await sleep(100) // Evaluate hover at key points const hoverResult = await evalJs<{ centerHover: string | null outsideHover: string | null leftEdgeHover: string | null rightEdgeHover: string | null }>(`(() => { const inst = window.__d2webHudInstance; const rect = inst.hudCanvas.getBoundingClientRect(); // Calculate client coords for center const scale = Math.min(rect.width / 800, rect.height / 600); const offX = Math.max(0, (rect.width - 800 * scale) / 2); const offY = Math.max(0, rect.height - 600 * scale); const clientCenterX = 316 * scale + offX; const clientCenterY = 562 * scale + offY; const mappedCenter = inst.clientToLogical(clientCenterX, clientCenterY); inst.controlBar.handleMouseMove(mappedCenter.x, mappedCenter.y); const centerHover = inst.controlBar.hoveredElement; // Test point outside (above groove) const clientOutsideY = 550 * scale + offY; const mappedOutside = inst.clientToLogical(clientCenterX, clientOutsideY); inst.controlBar.handleMouseMove(mappedOutside.x, mappedOutside.y); const outsideHover = inst.controlBar.hoveredElement; // Left edge (x = 256.5 to avoid IEEE 754 epsilon boundary) const clientLeftX = 256.5 * scale + offX; const mappedLeft = inst.clientToLogical(clientLeftX, clientCenterY); inst.controlBar.handleMouseMove(mappedLeft.x, mappedLeft.y); const leftEdgeHover = inst.controlBar.hoveredElement; // Right edge (x = 375.5) const clientRightX = 375.5 * scale + offX; const mappedRight = inst.clientToLogical(clientRightX, clientCenterY); inst.controlBar.handleMouseMove(mappedRight.x, mappedRight.y); const rightEdgeHover = inst.controlBar.hoveredElement; return { centerHover, outsideHover, leftEdgeHover, rightEdgeHover, }; })()`) console.log(`Resolution ${res.w}x${res.h} @ DPR ${dpr}:`, hoverResult) } } } finally { chromeProc.kill('SIGKILL') await server.close() } } main().catch(err => { console.error(err) process.exit(1) })