224 lines
8.3 KiB
TypeScript
224 lines
8.3 KiB
TypeScript
/**
|
|
* 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<void>(resolve => setTimeout(resolve, ms))
|
|
|
|
async function main(): Promise<void> {
|
|
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<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
|
|
}
|
|
|
|
const saveScreenshot = async (filename: string): Promise<void> => {
|
|
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>(
|
|
`Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready && window.__d2webHud?.assetsLoaded?.ctrlPnl)`,
|
|
)
|
|
if (ready) break
|
|
await sleep(300)
|
|
}
|
|
await sleep(600)
|
|
|
|
const initialHud = await evalJs<any>(`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<any>(`(() => {
|
|
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<any>(`(() => {
|
|
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<any>(`(() => {
|
|
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<any>(`(() => {
|
|
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)
|
|
})
|