89 lines
3.9 KiB
JavaScript
89 lines
3.9 KiB
JavaScript
// 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}`)
|
|
}
|