up
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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,
|
||||
[
|
||||
`--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
|
||||
})
|
||||
Reference in New Issue
Block a user