/** * Automated Headless Browser Verification & Visual Audit for Issue #27 (`acts.html`). * * Uses Vite dev server + `/usr/bin/google-chrome` over native WebSocket CDP (zero external deps). */ import { spawn } from 'node:child_process' import { mkdirSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { createServer } from 'vite' const ARTIFACT_DIR = process.argv[2] || '/usr/local/google/home/taodao/.gemini/jetski/brain/aa680fb2-b60b-4fb6-86df-d401210969d8' const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) async function main(): Promise { mkdirSync(ARTIFACT_DIR, { recursive: true }) 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 dev server listening at ${baseUrl}`) const debugPort = 9237 const chromeProc = spawn('/usr/bin/google-chrome', [ '--headless=new', `--remote-debugging-port=${debugPort}`, '--no-sandbox', '--disable-dev-shm-usage', '--enable-webgl', '--ignore-gpu-blocklist', '--use-gl=angle', '--use-angle=swiftshader', '--window-size=1280,840', `${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 { // Wait for Chrome CDP } } if (!wsUrl) { throw new Error('Failed to connect to Chrome CDP') } console.log('2. Connected to Chrome CDP WebSocket:', 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 } const saveScreenshot = async (filename: string): Promise => { const shot = await sendCommand<{ data: string }>('Page.captureScreenshot', { format: 'png' }) writeFileSync(join(ARTIFACT_DIR, filename), Buffer.from(shot.data, 'base64')) console.log(` Saved screenshot: ${filename}`) } await sendCommand('Runtime.enable') await sendCommand('Page.enable') // Wait for both ActScene and HudManager to become ready for (let i = 0; i < 120; i++) { const ready = await evalJs( `Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready && window.__d2webHud?.assetsLoaded?.ctrlPnl)`, ) if (ready) break await sleep(300) } await sleep(600) const initialHud = await evalJs(`window.__d2webHud`) console.log('3. Verified initial __d2webHud state:', JSON.stringify(initialHud)) if (!initialHud || initialHud.dc6DecodeFailures !== 0) { throw new Error(`Expected 0 DC6 decode failures, got: ${JSON.stringify(initialHud)}`) } for (const [k, v] of Object.entries(initialHud.assetsLoaded)) { if (!v) throw new Error(`Asset group failed to load: ${k}`) } // Screenshot 1: Base Town HUD await saveScreenshot('hud_base_town.png') // Screenshot 2: Left Character Sheet (`A`) + Right Inventory (`I`) + Hover Harlequin Crest const dualState = await evalJs(`(() => { const inst = window.__d2webHudInstance; inst.toggleLeftPanel('char'); inst.toggleRightPanel('inv'); inst.inventory.handleMouseMove(561, 93); // Hover Harlequin Crest in Helm slot inst.render(performance.now(), false); return window.__d2webHud; })()`) if (dualState.leftPanel !== 'char' || dualState.rightPanel !== 'inv') { throw new Error(`Split panel assertion failed: ${JSON.stringify(dualState)}`) } await sleep(200) await saveScreenshot('hud_char_and_inv.png') // Verify Inventory Drag-and-Drop, Equipment Swap, Belt Drink, Stat & Skill Allocation const interactionCheck = await evalJs(`(() => { const inst = window.__d2webHudInstance; inst.inventory.clickEquipSlot('weapon1'); const pickedCode = inst.inventory.cursorItem?.code; inst.inventory.clickGridCell(8, 0); const swappedCode = inst.inventory.cursorItem?.code; inst.inventory.clickEquipSlot('weapon1'); const finalWeapon1 = inst.inventory.equipped.weapon1?.code; inst.hp = 900; inst.drinkBeltSlot(0); const afterBeltCount = inst.belt.countTotalPotions(); const healedHp = inst.hp; inst.charSheet.allocateStat('vit'); inst.syncPublishedState(); return { pickedCode, swappedCode, finalWeapon1, afterBeltCount, healedHp, remainingStatPts: inst.state.statPoints }; })()`) console.log('4. Verified HUD interactions via CDP:', JSON.stringify(interactionCheck)) if ( interactionCheck.pickedCode !== 'oba' || interactionCheck.swappedCode !== 'crs' || interactionCheck.finalWeapon1 !== 'crs' || interactionCheck.afterBeltCount !== 15 || interactionCheck.healedHp !== 1080 ) { throw new Error(`HUD interaction check failed: ${JSON.stringify(interactionCheck)}`) } // Screenshot 3: Left Quest Log (`Q`) + Right Skill Tree (`T`) + Hover Frozen Orb const questSkillState = await evalJs(`(() => { const inst = window.__d2webHudInstance; inst.toggleLeftPanel('quest'); inst.toggleRightPanel('skill'); inst.skillTree.handleMouseMove(445, 422); // Hover Frozen Orb inst.render(performance.now(), false); return window.__d2webHud; })()`) if (questSkillState.leftPanel !== 'quest' || questSkillState.rightPanel !== 'skill') { throw new Error(`Quest/Skill panel assertion failed: ${JSON.stringify(questSkillState)}`) } await sleep(200) await saveScreenshot('hud_quest_and_skills.png') // Screenshot 4: Left Waypoint Menu (`V`) + 4x4 Pop-up Belt (`~`) + Right Skill Speedbar Popup + Monster Plaque const wpBeltState = await evalJs(`(() => { const inst = window.__d2webHudInstance; inst.toggleLeftPanel('waypoint'); inst.toggleRightPanel('skill'); // close right panel inst.belt.lockedOpen = true; inst.hotkeys.openPopup = 'right'; inst.hotkeys.hoveredPopupSkill = { side: 'right', skillId: 64 }; inst.targetMonster = { name: 'Corpsefire (尸体发火)', hp: 740, maxHp: 980, rank: 'superunique', affixes: ['Spectral Hit · 特别命中', 'Undead · 不死族'], }; inst.render(performance.now(), false); return window.__d2webHud; })()`) if (wpBeltState.leftPanel !== 'waypoint' || !wpBeltState.beltExpanded) { throw new Error(`Waypoint/Belt assertion failed: ${JSON.stringify(wpBeltState)}`) } await sleep(200) await saveScreenshot('hud_waypoint_and_belt.png') ws.close() console.log('5. ALL Issue #27 Browser & CDP Assertions Passed 100%!') } finally { chromeProc.kill() await server.close() } } main().catch((err) => { console.error('verify-ui-browser failed:', err) process.exit(1) })