297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
/**
|
|
* 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<void>(resolve => setTimeout(resolve, ms))
|
|
|
|
async function main(): Promise<void> {
|
|
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<void>(resolve => { ws.onopen = () => resolve() })
|
|
|
|
let idCounter = 1
|
|
const sendCommand = <T = any>(method: string, params: Record<string, unknown> = {}): Promise<T> =>
|
|
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 <T = any>(expression: string): Promise<T> => {
|
|
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>(`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)
|
|
})
|