feat(js): m09 input, app, runtime, browser, main, index.html and smoke test
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
584f1d4825
commit
8028bb2fc0
@@ -0,0 +1,88 @@
|
||||
// Headless smoke test: the page loads, throws no errors, and the canvas has painted something.
|
||||
// Zero dependencies: drives google-chrome over the DevTools protocol with Node's global WebSocket.
|
||||
// Usage: node scripts/smoke.mjs <url> exit 0 = painted and clean, 1 = problem (reason printed)
|
||||
import { spawn } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { createServer } from 'node:net'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const url = process.argv[2]
|
||||
if (!url) { console.error('usage: smoke.mjs <url>'); process.exit(2) }
|
||||
const chromePath = process.env.CHROME ?? '/usr/bin/google-chrome'
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
const freePort = () => new Promise((resolve, reject) => {
|
||||
const s = createServer()
|
||||
s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)) })
|
||||
s.on('error', reject)
|
||||
})
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), 'smoke-'))
|
||||
const port = await freePort()
|
||||
const chrome = spawn(chromePath, [
|
||||
'--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${dir}`,
|
||||
'--no-sandbox', '--disable-gpu', '--no-first-run', 'about:blank',
|
||||
], { stdio: 'ignore' })
|
||||
const cleanup = () => { chrome.kill('SIGKILL'); rmSync(dir, { recursive: true, force: true }) }
|
||||
const finish = (code, msg) => { if (msg) console.log(msg); cleanup(); process.exit(code) }
|
||||
process.on('SIGINT', () => finish(1, 'interrupted'))
|
||||
const timer = setTimeout(() => finish(1, 'FAIL: timed out'), 30000)
|
||||
|
||||
try {
|
||||
// Wait for the debugging endpoint, then find the page target.
|
||||
let target
|
||||
for (let i = 0; i < 100 && !target; i++) {
|
||||
try {
|
||||
const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json()
|
||||
target = list.find((t) => t.type === 'page')
|
||||
} catch { await sleep(100) }
|
||||
}
|
||||
if (!target) finish(1, 'FAIL: could not connect to Chrome')
|
||||
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl)
|
||||
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject })
|
||||
const errors = []
|
||||
const pending = new Map()
|
||||
let id = 0
|
||||
const send = (method, params = {}) => new Promise((resolve) => {
|
||||
pending.set(++id, resolve)
|
||||
ws.send(JSON.stringify({ id, method, params }))
|
||||
})
|
||||
ws.onmessage = ({ data }) => {
|
||||
const m = JSON.parse(data)
|
||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result ?? m.error); pending.delete(m.id) }
|
||||
else if (m.method === 'Runtime.exceptionThrown') {
|
||||
const d = m.params.exceptionDetails
|
||||
errors.push(`exception: ${d.exception?.description ?? d.text}`)
|
||||
} else if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') {
|
||||
errors.push(`console.error: ${m.params.args.map((a) => a.value ?? a.description).join(' ')}`)
|
||||
} else if (m.method === 'Log.entryAdded' && m.params.entry.level === 'error') {
|
||||
errors.push(`log: ${m.params.entry.text} ${m.params.entry.url ?? ''}`)
|
||||
}
|
||||
}
|
||||
await send('Runtime.enable')
|
||||
await send('Log.enable')
|
||||
await send('Page.enable')
|
||||
await send('Page.navigate', { url })
|
||||
await sleep(1000)
|
||||
|
||||
// Does the canvas hold at least one pixel that is not the #111111 background (and not transparent)?
|
||||
const check = `(() => {
|
||||
const c = document.querySelector('canvas')
|
||||
if (!c) return 'no canvas'
|
||||
const d = c.getContext('2d').getImageData(0, 0, c.width, c.height).data
|
||||
for (let i = 0; i < d.length; i += 4)
|
||||
if (d[i + 3] !== 0 && (d[i] !== 0x11 || d[i + 1] !== 0x11 || d[i + 2] !== 0x11)) return 'painted'
|
||||
return 'blank'
|
||||
})()`
|
||||
const res = await send('Runtime.evaluate', { expression: check, returnByValue: true })
|
||||
const state = res.result?.value ?? `evaluation failed: ${JSON.stringify(res)}`
|
||||
|
||||
const problems = [...(state === 'painted' ? [] : [`canvas: ${state}`]), ...errors]
|
||||
clearTimeout(timer)
|
||||
if (problems.length) finish(1, `FAIL ${url}\n ${problems.join('\n ')}`)
|
||||
finish(0, `OK ${url}: canvas painted, no errors`)
|
||||
} catch (e) {
|
||||
finish(1, `FAIL: ${e.message}`)
|
||||
}
|
||||
Reference in New Issue
Block a user