This commit is contained in:
2026-10-17 21:21:03 +07:00
parent 1d4028b183
commit 402e212332
15 changed files with 1017 additions and 24 deletions
+244
View File
@@ -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
})