diablo2-web/scripts/browser/inspect-page.mjs

94 lines
3.6 KiB
JavaScript

// Minimal CDP driver: node cdp_inspect.mjs <chromeBin> <url> [scriptFile]
// Launches headless Chromium, connects over the DevTools WebSocket (Node's
// built-in WebSocket — no dependencies), evaluates one async script, prints JSON.
import { spawn } from 'node:child_process'
import { mkdtempSync, readFileSync } from 'node:fs'
import { readFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const [chromeBin, targetUrl, scriptFile] = process.argv.slice(2)
const userDataDir = mkdtempSync(join(tmpdir(), 'cdp-'))
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Optional --window-size=WxH, so responsive behaviour can be checked at real sizes.
const windowSizeArg = process.argv.find((arg) => arg.startsWith('--window-size='))
const windowSize = windowSizeArg ? windowSizeArg.split('=')[1] : null
const chrome = spawn(chromeBin, [
'--headless=new', '--no-sandbox', '--disable-dev-shm-usage',
// Headless Chrome wants `--window-size=W,H`; a `WxH` string is accepted here too.
...(windowSize ? [`--window-size=${windowSize.replace('x', ',')}`] : []),
// Software WebGL2: headless has no GPU, and SwiftShader is the only way to
// exercise the renderer path in CI-like runs.
'--use-angle=swiftshader', '--use-gl=angle', '--enable-unsafe-swiftshader',
`--user-data-dir=${userDataDir}`,
'--remote-debugging-port=0',
targetUrl,
], { stdio: ['ignore', 'ignore', 'pipe'] })
let stderr = ''
chrome.stderr.on('data', (d) => { stderr += d })
let port = null
for (let i = 0; i < 100; i++) {
await sleep(200)
try {
port = readFileSync(join(userDataDir, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0]
if (port) break
} catch {}
}
if (!port) { console.error('NO PORT', stderr.slice(-500)); process.exit(1) }
let page
for (let i = 0; i < 50; i++) {
await sleep(200)
try {
const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json()
page = list.find((x) => x.type === 'page')
if (page) break
} catch {}
}
if (!page) { console.error('NO PAGE', stderr.slice(-500)); process.exit(1) }
const ws = new WebSocket(page.webSocketDebuggerUrl)
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject })
let msgId = 0
const pending = new Map()
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.id && pending.has(message.id)) { pending.get(message.id)(message); pending.delete(message.id) }
}
const send = (method, params = {}) => new Promise((resolve) => {
const id = ++msgId
pending.set(id, resolve)
ws.send(JSON.stringify({ id, method, params }))
})
await send('Runtime.enable')
await send('Page.enable')
await sleep(3000)
const expression = scriptFile
? await readFile(scriptFile, 'utf8')
: `({ title: document.title, text: document.body.innerText.slice(0, 2000) })`
const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })
const shotIndex = process.argv.indexOf('--screenshot')
if (shotIndex !== -1) {
const shotPath = process.argv[shotIndex + 1]
const shot = await send('Page.captureScreenshot', { format: 'png' })
const data = shot.result?.data
if (typeof data === 'string') {
const { writeFileSync } = await import('node:fs')
writeFileSync(shotPath, Buffer.from(data, 'base64'))
console.error(`screenshot written: ${shotPath}`)
}
}
if (result.result?.exceptionDetails) {
console.error('EVAL ERROR', JSON.stringify(result.result.exceptionDetails).slice(0, 1500))
} else {
console.log(JSON.stringify(result.result?.result?.value, null, 2))
}
ws.close()
chrome.kill()
process.exit(0)