246 lines
8.7 KiB
JavaScript
246 lines
8.7 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const { spawn } = require('node:child_process')
|
|
const { mkdtemp, mkdir, readFile, writeFile, rm } = require('node:fs/promises')
|
|
const { join, resolve } = require('node:path')
|
|
const { tmpdir } = require('node:os')
|
|
const { fileHash } = require('./release.cjs')
|
|
const metadata = require('../package.json')
|
|
const root = resolve(__dirname, '..')
|
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
async function until(check, label) {
|
|
for (let i = 0; i < 100; i++) {
|
|
const result = await check()
|
|
if (result) return result
|
|
await delay(100)
|
|
}
|
|
throw new Error(`Timeout: ${label}`)
|
|
}
|
|
async function launch(binary, profile, check) {
|
|
const child = spawn(
|
|
binary,
|
|
[
|
|
...(process.platform === 'linux' ? ['--ozone-platform=x11'] : []),
|
|
`--user-data-dir=${profile}`,
|
|
'--remote-debugging-port=0',
|
|
'--remote-debugging-address=127.0.0.1',
|
|
'--no-sandbox',
|
|
'--mute-audio',
|
|
'--disable-gpu',
|
|
],
|
|
{ stdio: ['ignore', 'ignore', 'pipe'] },
|
|
)
|
|
let stderr = ''
|
|
let spawnError = null
|
|
let exited = false
|
|
child.on('error', (error) => {
|
|
spawnError = error
|
|
})
|
|
const exit = new Promise((resolve) =>
|
|
child.on('close', () => {
|
|
exited = true
|
|
resolve()
|
|
}),
|
|
)
|
|
child.stderr.on('data', (bytes) => {
|
|
stderr = (stderr + bytes.toString()).slice(-32768)
|
|
})
|
|
let socket
|
|
try {
|
|
const port = await until(() => {
|
|
if (spawnError) throw spawnError
|
|
if (exited) throw new Error('Packaged application exited before startup')
|
|
const match = /DevTools listening on ws:\/\/127\.0\.0\.1:(\d+)\//.exec(stderr)
|
|
return match?.[1]
|
|
}, 'DevTools endpoint')
|
|
const target = await until(async () => {
|
|
const response = await fetch(`http://127.0.0.1:${port}/json`)
|
|
const targets = await response.json()
|
|
return targets.find((page) => page.url === 'screen-app://app/index.html')
|
|
}, 'control target')
|
|
socket = new WebSocket(target.webSocketDebuggerUrl)
|
|
await new Promise((resolve, reject) => {
|
|
socket.onopen = resolve
|
|
socket.onerror = reject
|
|
})
|
|
let id = 0
|
|
const pending = new Map()
|
|
socket.onmessage = ({ data }) => {
|
|
const result = JSON.parse(data)
|
|
const entry = pending.get(result.id)
|
|
if (entry) {
|
|
pending.delete(result.id)
|
|
clearTimeout(entry.timer)
|
|
entry.resolve(result)
|
|
}
|
|
}
|
|
socket.onclose = () => {
|
|
for (const entry of pending.values()) {
|
|
clearTimeout(entry.timer)
|
|
entry.reject(new Error('Debugger disconnected'))
|
|
}
|
|
pending.clear()
|
|
}
|
|
const request = (method, params = {}) =>
|
|
new Promise((resolve, reject) => {
|
|
const callId = ++id
|
|
const timer = setTimeout(() => {
|
|
pending.delete(callId)
|
|
reject(new Error('Debugger request timeout'))
|
|
}, 5000)
|
|
pending.set(callId, { resolve, reject, timer })
|
|
socket.send(JSON.stringify({ id: callId, method, params }))
|
|
})
|
|
const evaluate = async (expression) => {
|
|
const result = await request('Runtime.evaluate', {
|
|
expression,
|
|
awaitPromise: true,
|
|
returnByValue: true,
|
|
})
|
|
assert.ok(!result.error && !result.result?.exceptionDetails, 'Packaged evaluation failed')
|
|
return result.result.result.value
|
|
}
|
|
await until(
|
|
() => evaluate('!!window.screenBridge && !!document.querySelector("h1")'),
|
|
'packaged UI',
|
|
)
|
|
await check(evaluate)
|
|
void request('Page.close').catch(() => {})
|
|
await until(() => exited, 'normal application exit')
|
|
} finally {
|
|
socket?.close()
|
|
if (!exited) {
|
|
child.kill('SIGKILL')
|
|
await exit
|
|
}
|
|
}
|
|
}
|
|
async function main() {
|
|
const binary = resolve(process.argv[2] || 'release/linux-unpacked/evening-detective-screen')
|
|
const previous = process.argv[3] ? resolve(process.argv[3]) : null
|
|
const profile = await mkdtemp(join(tmpdir(), 'screen-packaged-'))
|
|
const { PackageStore } = require('../dist-electron/electron/package-store.js')
|
|
const { buildScreenPackage } = require('../dist-electron/electron/screen-package.js')
|
|
try {
|
|
const store = new PackageStore(join(profile, 'packages'), {
|
|
fetch: async (input) => {
|
|
const video = String(input).endsWith('.mp4')
|
|
return new Response(
|
|
await readFile(join(root, 'fixtures', video ? 'video.mp4' : 'audio.wav')),
|
|
{ headers: { 'Content-Type': video ? 'video/mp4' : 'audio/wav' } },
|
|
)
|
|
},
|
|
})
|
|
await store.initialize()
|
|
const description = buildScreenPackage('https://packaged.test', 7, {
|
|
game: {
|
|
game: {
|
|
ID: 7,
|
|
name: 'Packaged offline',
|
|
scenario: {
|
|
story: { introduction: { audio: 'intro.wav' }, conclusion: { video: 'video.mp4' } },
|
|
},
|
|
},
|
|
},
|
|
plan: {},
|
|
music: {},
|
|
})
|
|
const pkg = await store.download(description)
|
|
const connection = join(profile, 'connection')
|
|
await mkdir(connection)
|
|
await writeFile(
|
|
join(connection, 'selection.json'),
|
|
JSON.stringify({ server: description.server, key: pkg.key }),
|
|
)
|
|
const results = {
|
|
packageKey: pkg.key,
|
|
capturedAt: '2026-10-17T10:00:00.000Z',
|
|
questions: [{ code: 'A', text: 'Question' }],
|
|
teams: [
|
|
{
|
|
id: 't',
|
|
name: 'Team',
|
|
place: 1,
|
|
totalScore: 3,
|
|
actionsCount: 1,
|
|
answers: [{ questionCode: 'A', score: 3 }],
|
|
},
|
|
],
|
|
}
|
|
const resultsFile = join(connection, `results-${pkg.key}.json`)
|
|
await writeFile(resultsFile, JSON.stringify(results))
|
|
await writeFile(
|
|
join(profile, 'windows.json'),
|
|
JSON.stringify({
|
|
version: 1,
|
|
control: { bounds: { x: 30, y: 30, width: 900, height: 650 }, maximized: false },
|
|
display: null,
|
|
}),
|
|
)
|
|
const manifest = join(profile, 'packages', pkg.key, `v-${pkg.version}`, 'manifest.json')
|
|
const before = await fileHash(manifest)
|
|
const resultHash = await fileHash(resultsFile)
|
|
if (previous)
|
|
await launch(previous, profile, async (evaluate) => {
|
|
assert.equal(await evaluate('typeof window.require'), 'undefined')
|
|
})
|
|
for (let attempt = 0; attempt < 2; attempt++)
|
|
await launch(binary, profile, async (evaluate) => {
|
|
assert.equal(await evaluate('typeof window.require'), 'undefined')
|
|
const state = await evaluate('window.screenBridge.state()')
|
|
assert.equal(state.catalog.key, pkg.key)
|
|
assert.equal(state.results.teams[0].totalScore, 3)
|
|
assert.equal(state.outputOpen, false)
|
|
assert.equal(state.timer.status, 'stopped')
|
|
assert.equal(state.channels.main.desired, 'stopped')
|
|
assert.equal(state.channels.music.desired, 'stopped')
|
|
assert.equal(await evaluate('document.querySelectorAll("audio,video").length'), 0)
|
|
for (const url of [
|
|
'screen-app://app/media/video',
|
|
state.catalog.media.find((m) => m.kind === 'video').url,
|
|
]) {
|
|
const range = await evaluate(
|
|
`fetch(${JSON.stringify(url)}, {headers:{Range:'bytes=100-199'}}).then(async r=>({status:r.status,size:(await r.arrayBuffer()).byteLength}))`,
|
|
)
|
|
assert.deepEqual(range, { status: 206, size: 100 })
|
|
}
|
|
const logs = await readFile(join(profile, 'logs', 'diagnostic.jsonl'), 'utf8')
|
|
const starts = logs
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => JSON.parse(line))
|
|
.filter((row) => row.event === 'app-start')
|
|
assert.equal(starts.at(-1).version, metadata.version)
|
|
assert.equal(await fileHash(manifest), before)
|
|
assert.equal(await fileHash(resultsFile), resultHash)
|
|
assert.equal((await evaluate('window.screenBridge.host()')).sleepBlocked, false)
|
|
})
|
|
const log = (await readFile(join(profile, 'logs', 'diagnostic.jsonl'), 'utf8'))
|
|
.trim()
|
|
.split('\n')
|
|
.map((line) => JSON.parse(line))
|
|
assert.equal(log.at(-1).event, 'app-stop')
|
|
const restored = new PackageStore(join(profile, 'packages'))
|
|
await restored.initialize()
|
|
assert.equal(restored.list()[0].version, pkg.version)
|
|
const windows = JSON.parse(await readFile(join(profile, 'windows.json'), 'utf8'))
|
|
assert.equal(windows.control.maximized, false)
|
|
assert.ok(windows.control.bounds.width <= 900 && windows.control.bounds.height <= 650)
|
|
console.log(
|
|
'PACKAGED_SMOKE_PASS',
|
|
JSON.stringify({
|
|
version: metadata.version,
|
|
previous: !!previous,
|
|
restarts: 2,
|
|
offlineFiles: pkg.files.length,
|
|
preservedResults: true,
|
|
}),
|
|
)
|
|
} finally {
|
|
await rm(profile, { recursive: true, force: true })
|
|
}
|
|
}
|
|
main().catch((error) => {
|
|
console.error(error.message)
|
|
process.exitCode = 1
|
|
})
|