Files
evening_detective_screen/scripts/smoke.cjs
T
2026-10-18 04:03:44 +07:00

680 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const assert = require('node:assert/strict')
const { app, BrowserWindow, screen, nativeImage, powerSaveBlocker } = require('electron')
const { join, resolve } = require('node:path')
const { mkdtempSync, readFileSync, writeFileSync } = require('node:fs')
const root = resolve(__dirname, '..')
app.setAppPath(root)
app.setPath('userData', mkdtempSync(join(require('node:os').tmpdir(), 'screen-smoke-')))
const manual = process.argv.includes('--manual')
const delay = (ms) => new Promise((r) => setTimeout(r, ms))
let targetDisplay
let isSyntheticDisplay = false
const boundsBeforeFullscreen = new Map()
const fullscreenEntered = new Set()
const powerBlocks = { active: new Set(), starts: 0, stops: 0 }
const startBlocker = powerSaveBlocker.start.bind(powerSaveBlocker)
const stopBlocker = powerSaveBlocker.stop.bind(powerSaveBlocker)
powerSaveBlocker.start = (type) => {
assert.equal(type, 'prevent-display-sleep')
const id = startBlocker(type)
powerBlocks.active.add(id)
powerBlocks.starts++
return id
}
powerSaveBlocker.stop = (id) => {
assert.ok(powerBlocks.active.delete(id), 'blocker released exactly once')
powerBlocks.stops++
return stopBlocker(id)
}
const captures = []
app.on('browser-window-created', (_event, window) => {
window.webContents.on('render-process-gone', (_event, details) =>
console.log('SMOKE_RENDERER_GONE', window.id, details.reason),
)
const capturePage = window.webContents.capturePage.bind(window.webContents)
window.webContents.capturePage = (...args) => {
captures.push({ id: window.id, time: performance.now() })
return capturePage(...args)
}
window.on('enter-full-screen', () => fullscreenEntered.add(window.id))
let requestedBounds
const setBounds = window.setBounds.bind(window)
window.setBounds = (bounds, ...args) => {
requestedBounds = { ...bounds }
return setBounds(bounds, ...args)
}
const setFullScreen = window.setFullScreen.bind(window)
window.setFullScreen = (flag) => {
if (flag) boundsBeforeFullscreen.set(window.id, requestedBounds)
// WM не знает синтетический монитор: native fullscreen проверяем только на реальном.
if (isSyntheticDisplay) {
queueMicrotask(() => {
if (!window.isDestroyed()) window.emit(flag ? 'enter-full-screen' : 'leave-full-screen')
})
return
}
return setFullScreen(flag)
}
})
app.whenReady().then(() => {
const original = screen.getAllDisplays.bind(screen)
const displays = original()
targetDisplay = displays.find((display) => display.id !== screen.getPrimaryDisplay().id)
if (!targetDisplay && manual) {
console.error('Подключите внешний дисплей в режиме расширенного рабочего стола')
app.exit(1)
return
}
if (!targetDisplay) {
isSyntheticDisplay = true
targetDisplay = {
...displays[0],
id: 424242,
label: 'Synthetic test display',
bounds: { x: 1280, y: 0, width: 1280, height: 720 },
size: { width: 1280, height: 720 },
}
screen.getAllDisplays = () => [...original(), targetDisplay]
}
})
function musicWav(frequency) {
const size = 48000 * 2 * 4
const bytes = Buffer.alloc(44 + size)
bytes.write('RIFF', 0)
bytes.writeUInt32LE(36 + size, 4)
bytes.write('WAVEfmt ', 8)
bytes.writeUInt32LE(16, 16)
bytes.writeUInt16LE(1, 20)
bytes.writeUInt16LE(1, 22)
bytes.writeUInt32LE(48000, 24)
bytes.writeUInt32LE(96000, 28)
bytes.writeUInt16LE(2, 32)
bytes.writeUInt16LE(16, 34)
bytes.write('data', 36)
bytes.writeUInt32LE(size, 40)
for (let sample = 0; sample < size / 2; sample++)
bytes.writeInt16LE(
Math.round(Math.sin((2 * Math.PI * frequency * sample) / 48000) * 2000),
44 + sample * 2,
)
return bytes
}
let offlinePackage
const seeded = (async () => {
const { PackageStore } = require(root + '/dist-electron/electron/package-store.js')
const { buildScreenPackage } = require(root + '/dist-electron/electron/screen-package.js')
const audio = readFileSync(root + '/fixtures/audio.wav')
const video = readFileSync(root + '/fixtures/video.mp4')
const image = (color) =>
nativeImage
.createFromBitmap(Buffer.alloc(320 * 180 * 4).fill(Buffer.from(color)), {
width: 320,
height: 180,
})
.toPNG()
const cover = image([30, 60, 80, 255])
const background = image([70, 40, 20, 255])
const pkg = buildScreenPackage('https://offline.invalid', 7, {
game: {
game: {
ID: 7,
name: 'Offline smoke',
scenario: {
image: 'cover.png',
story: {
background: 'background.png',
introduction: { audio: 'intro.wav' },
conclusion: { video: 'ending.mp4' },
},
},
},
},
plan: {
plan: {
steps: Array.from({ length: 8 }, (_, index) => ({
mode: index + 1,
text: index === 7 ? 'Привет, детективы!\nКириллица: Ёжик и улика №7' : '',
timerLabel: 'Перерыв',
timerDurationSeconds: index === 6 ? '2' : '0',
})),
},
},
music: {
gamePlaylistId: 1,
playlists: [
{
ID: 1,
name: 'Offline playlist',
tracks: [
{ ID: 1, name: 'One', filename: 'one.wav', position: 0 },
{ ID: 2, name: 'Two', filename: 'two.wav', position: 1 },
],
},
],
},
})
const store = new PackageStore(join(app.getPath('userData'), 'packages'), {
fetch: async (input) => {
const url = String(input)
const bytes = url.endsWith('cover.png')
? cover
: url.endsWith('background.png')
? background
: url.endsWith('intro.wav')
? audio
: url.endsWith('.mp4')
? video
: musicWav(url.endsWith('two.wav') ? 660 : 330)
return new Response(bytes, {
headers: {
'Content-Type': url.endsWith('.png')
? 'image/png'
: url.endsWith('.mp4')
? 'video/mp4'
: 'audio/wav',
'Content-Length': String(bytes.length),
},
})
},
})
await store.initialize()
offlinePackage = await store.download(pkg)
})()
seeded.catch((error) => {
console.error('PACKAGE_SEED_FAIL', error)
app.exit(1)
})
// Регистрация протокола до ready; новый экземпляр хранилища открывается после seed.
const whenReady = app.whenReady.bind(app)
app.whenReady = () => whenReady().then(() => seeded)
require(root + '/dist-electron/electron/main.js')
app.whenReady = whenReady
async function poll(check, label) {
for (let i = 0; i < 150; i++) {
const result = await check()
if (result) return result
await delay(100)
}
throw new Error('Timeout: ' + label)
}
app.whenReady().then(async () => {
try {
await seeded
const control = await poll(() => BrowserWindow.getAllWindows()[0], 'control')
if (process.platform === 'linux')
assert.ok(control.getNativeWindowHandle().readUInt32LE(0) > 0xffff, 'native X11 window ID')
control.webContents.on('console-message', (_event, details) =>
console.log('renderer:', details.message),
)
const evaluate = (source) => control.webContents.executeJavaScript(source)
await poll(
async () =>
!control.webContents.isLoading() &&
(await evaluate('!!window.screenBridge && !!document.querySelector("h1")')),
'control UI',
)
if (manual) {
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
console.log(
'SCREEN004_ACCEPTANCE_READY',
JSON.stringify({ userData: app.getPath('userData'), electron: process.versions.electron }),
)
return
}
const state = () => evaluate('window.screenBridge.state()')
const command = async (intent) => {
const sequence = await evaluate(
`(async () => { const state = await window.screenBridge.state(); const sequence = state.sequence + 1; await window.screenBridge.command({ session: state.session, sequence, ...${JSON.stringify(intent)} }); return sequence })()`,
)
return poll(async () => {
const s = await state()
const c = intent.channel ? s.channels[intent.channel] : null
if (c?.status === 'error') throw new Error(c.error)
return s.sequence >= sequence && (!c || c.status !== 'pending') && s
}, JSON.stringify(intent))
}
assert.equal(await evaluate('typeof window.require'), 'undefined')
assert.equal((await evaluate('window.screenBridge.packages()')).length, 1)
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
let output = await poll(
() => BrowserWindow.getAllWindows().find((w) => w !== control),
'output',
)
const inspect = (source) =>
BrowserWindow.getAllWindows()
.find((w) => w !== control)
.webContents.executeJavaScript(source)
await poll(async () => (await state()).channels.main.status === 'stopped', 'fixture prepared')
if (process.platform === 'linux')
assert.equal(app.commandLine.getSwitchValue('ozone-platform'), 'x11')
async function checkDisplay(window) {
await poll(() => boundsBeforeFullscreen.has(window.id), 'fullscreen requested')
assert.deepEqual(boundsBeforeFullscreen.get(window.id), targetDisplay.bounds)
if (!isSyntheticDisplay) {
await poll(() => fullscreenEntered.has(window.id), 'fullscreen entered')
assert.ok(window.isFullScreen())
assert.equal(screen.getDisplayMatching(window.getBounds()).id, targetDisplay.id)
}
}
await checkDisplay(output)
await command({ action: 'play', channel: 'main' })
await poll(async () => (await state()).channels.main.status === 'playing', 'fixture play')
await command({ action: 'pause', channel: 'main' })
await command({ action: 'resume', channel: 'main' })
await command({ action: 'stop', channel: 'main' })
await command({ action: 'select', channel: 'main', sourceId: 'fixture-video' })
await command({ action: 'play', channel: 'main' })
await command({ action: 'seek', channel: 'main', position: 30 })
const fixturePosition = await inspect('document.querySelector("video").currentTime')
assert.ok(fixturePosition >= 29.9 && fixturePosition < 35)
const range = await inspect(
`fetch('screen-app://app/media/video', { headers: { Range: 'bytes=100-199' } }).then(async r => ({ status: r.status, size: (await r.arrayBuffer()).byteLength }))`,
)
assert.deepEqual(range, { status: 206, size: 100 })
await command({ action: 'pause', channel: 'main' })
assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false)
assert.equal(await evaluate('document.body.textContent.includes("Вступление и видео")'), false)
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
await poll(async () => {
const s = await state()
return (
s.catalog.key === offlinePackage.key &&
s.channels.main.status === 'stopped' &&
s.channels.music.status === 'stopped'
)
}, 'package prepared')
await command({ action: 'play', channel: 'main' })
await command({ action: 'seek', channel: 'main', position: 15 })
await command({ action: 'pause', channel: 'main' })
const pausedAt = await inspect(
'document.querySelector("[data-channel=main-audio]").currentTime',
)
await command({ action: 'resume', channel: 'main' })
assert.ok(
await inspect(
`document.querySelector('[data-channel=main-audio]').currentTime >= ${pausedAt - 0.1}`,
),
)
await command({ action: 'replay', channel: 'main' })
assert.ok(await inspect("document.querySelector('[data-channel=main-audio]').currentTime < 2"))
await command({ action: 'select', channel: 'main', sourceId: 'conclusion' })
await command({ action: 'play', channel: 'main' })
await command({ action: 'seek', channel: 'main', position: 30 })
await command({ action: 'play', channel: 'music' })
assert.equal(
await inspect(
"Array.from(document.querySelectorAll('audio,video')).filter(m => !m.paused).length",
),
2,
)
await command({ action: 'volume', channel: 'music', volume: 70 })
const videoBeforeVolume = await inspect('document.querySelector("video").currentTime')
await command({ action: 'volume', channel: 'main', volume: 25 })
assert.ok(
await inspect(`document.querySelector('video').currentTime >= ${videoBeforeVolume - 0.1}`),
)
await command({ action: 'mute', enabled: true })
assert.equal(
await inspect(
"Array.from(document.querySelectorAll('audio,video')).filter(m => !m.paused && !m.muted).length",
),
0,
)
await command({ action: 'mute', enabled: false })
assert.equal(await inspect("document.querySelector('video').volume"), 0.25)
await command({ action: 'seek', channel: 'music', position: 3.9 })
await poll(async () => {
const c = (await state()).channels.music
return c.trackIndex === 1 && c.status === 'playing'
}, 'automatic next track')
assert.equal(
await inspect(
"Array.from(document.querySelectorAll('audio[data-channel^=music]')).filter(m => !m.paused).length",
),
1,
)
await command({ action: 'seek', channel: 'music', position: 3.9 })
await poll(
async () => (await state()).channels.music.status === 'ended',
'playlist ends without repeat',
)
await command({ action: 'loop', channel: 'music', enabled: true })
await command({ action: 'resume', channel: 'music' })
await command({ action: 'seek', channel: 'music', position: 3.9 })
await poll(async () => {
const c = (await state()).channels.music
return c.trackIndex === 0 && c.status === 'playing'
}, 'playlist repeats')
await command({ action: 'pause', channel: 'music' })
await command({ action: 'next', channel: 'music' })
assert.equal((await state()).channels.music.trackIndex, 1)
assert.equal((await state()).channels.music.status, 'paused')
await command({ action: 'stop', channel: 'main' })
await evaluate(`(async () => {
const s = await window.screenBridge.state()
await Promise.all([{action:'play'}, {action:'seek',position:20}, {action:'pause'}].map((intent,i) => window.screenBridge.command({session:s.session,sequence:s.sequence+i+1,channel:'main',...intent})))
})()`)
await poll(
async () => (await state()).channels.main.status === 'paused',
'rapid commands settle',
)
assert.ok(await inspect('Math.abs(document.querySelector("video").currentTime - 20) < 0.3'))
const beforeDuplicate = await state()
await evaluate(
`window.screenBridge.command(${JSON.stringify({ session: beforeDuplicate.session, sequence: beforeDuplicate.sequence, action: 'replay', channel: 'main' })})`,
)
assert.equal((await state()).channels.main.operation, beforeDuplicate.channels.main.operation)
await assert.rejects(
inspect(
`window.screenBridge.command({session:'${beforeDuplicate.session}',sequence:999999,action:'play',channel:'main'})`,
),
)
await assert.rejects(inspect('window.screenBridge.packages()'))
await assert.rejects(
evaluate(
`window.screenBridge.command({session:'${beforeDuplicate.session}',sequence:999999,action:'select',channel:'main',sourceId:'arbitrary-file'})`,
),
)
assert.ok(
await inspect(
"Array.from(document.querySelectorAll('audio,video')).every(m => !m.getAttribute('src') || m.src.startsWith('screen-app://app/packages/'))",
),
)
await command({ action: 'play', channel: 'music' })
const modes = []
const sceneReady = (mode) =>
poll(async () => {
const s = await state()
if (s.scene.status === 'error') throw new Error(s.scene.error)
return s.scene.mode === mode && s.scene.status === 'ready' && s
}, `scene ${mode} ready`)
for (let index = 0; index < 8; index++) {
await command({ action: 'step', index })
const s = await sceneReady(index + 1)
modes.push(s.scene.mode)
assert.equal(s.scene.stepIndex, index)
assert.equal(s.channels.main.desired, 'stopped')
assert.equal(s.channels.music.desired, 'playing')
assert.equal(
await inspect('document.querySelectorAll("button,input,textarea,select").length'),
0,
)
if (index === 0)
assert.equal(await inspect('document.querySelector("img.cover").naturalWidth'), 320)
else assert.equal(await inspect('document.querySelector("img.background").naturalWidth'), 320)
if (index === 1 || index === 3) {
const clickMedia = (label) =>
evaluate(`(() => {
const row = document.querySelector('[data-step-index="${index}"]')
const button = Array.from(row.querySelectorAll('button')).find(b => b.textContent.trim() === ${JSON.stringify(label)})
if (!button || button.disabled) throw new Error('Main control unavailable')
button.click()
})()`)
await clickMedia('Запуск')
await poll(
async () => (await state()).channels.main.status === 'playing',
'plan media plays',
)
await clickMedia('Пауза')
await poll(
async () => (await state()).channels.main.status === 'paused',
'plan media paused',
)
await evaluate(`(() => {
const input = document.querySelector('#position-main')
input.focus(); input.value = '1'
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }))
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
input.blur()
})()`)
await clickMedia('Перемотать')
await poll(
async () => Math.abs((await state()).channels.main.position - 1) < 0.1,
'plan media seek',
)
await evaluate(`(() => {
const input = document.querySelector('#volume-main')
input.focus(); input.value = '42'
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' }))
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
input.blur()
})()`)
await poll(async () => (await state()).channels.main.volume === 42, 'plan media volume')
assert.equal((await state()).channels.main.desired, 'paused')
assert.equal((await state()).channels.music.desired, 'playing')
await clickMedia('Стоп')
await poll(
async () => (await state()).channels.main.status === 'stopped',
'plan media stopped',
)
}
if (index === 2) assert.ok(await inspect('document.body.textContent.includes("Успехов")'))
if (index === 4)
assert.ok(await inspect('document.body.textContent.includes("Спасибо за игру!")'))
if (index === 5)
assert.ok(
await inspect('document.body.textContent.includes("Результаты пока не загружены")'),
)
if (index === 6) {
assert.equal(s.timer.status, 'stopped')
assert.equal(await inspect('document.querySelector(".timer strong").textContent'), '00:02')
}
if (index === 7)
assert.ok(await inspect('document.body.textContent.includes("Кириллица: Ёжик и улика №7")'))
}
assert.equal(
await evaluate(
`!!document.querySelector(${JSON.stringify('#scene-text, #timer-label, #timer-seconds, [aria-label="Режим показа"]')})`,
),
false,
)
assert.equal(await evaluate('document.body.textContent.includes("Текст и таймер")'), false)
const safeText = 'Привет\n<script>window.__unsafe = true</script>'
await command({ action: 'text', text: safeText })
await sceneReady(8)
assert.ok(await inspect(`document.querySelector('.text').textContent.includes('<script>')`))
assert.equal(await inspect('typeof window.__unsafe'), 'undefined')
assert.equal(await inspect('document.fonts.check("16px system-ui", "Кириллица Ёжик №7")'), true)
await command({ action: 'timer-set', label: 'Перерыв', durationSeconds: 2 })
await sceneReady(7)
await command({ action: 'timer-start' })
await delay(300)
await command({ action: 'timer-pause' })
const remaining = (await state()).timer.remainingMs
await delay(300)
assert.equal((await state()).timer.remainingMs, remaining)
await command({ action: 'timer-resume' })
await command({ action: 'mode', mode: 1 })
await sceneReady(1)
await poll(
async () => (await state()).timer.status === 'finished',
'timer finishes while hidden',
)
await command({ action: 'mode', mode: 7 })
await sceneReady(7)
assert.equal(await inspect('document.querySelector(".timer strong").textContent'), '00:00')
await command({ action: 'timer-reset' })
await poll(
async () =>
(await inspect('document.querySelector(".timer strong").textContent')) === '00:02',
'timer reset DOM',
)
control.focus()
await poll(
() =>
evaluate(
`(() => { const image = document.querySelector('[data-testid=preview]'); return !!image && image.complete && image.naturalWidth > 0 && image.naturalWidth <= 960 && image.src.startsWith('data:image/png') })()`,
),
'native preview image without network',
)
assert.equal(await evaluate('document.querySelectorAll("audio,video").length'), 0)
await assert.rejects(inspect('window.screenBridge.preview()'))
const sceneBefore = (await state()).scene
await evaluate(
`(async () => { const s = await window.screenBridge.state(); window.screenBridge.reportScene({session:s.session, revision:s.scene.revision, status:'error',error:'forged'}) })()`,
)
await delay(50)
assert.equal((await state()).scene.status, sceneBefore.status)
const backgroundId = offlinePackage.package.media.background
const backgroundPath = join(
app.getPath('userData'),
'packages',
offlinePackage.key,
`v-${offlinePackage.version}`,
'assets',
backgroundId,
)
const originalImage = readFileSync(backgroundPath)
writeFileSync(backgroundPath, Buffer.alloc(originalImage.length))
await command({ action: 'text', text: 'Проверка изображения' })
await poll(
async () => (await state()).scene.status === 'error',
'broken image error on control',
)
assert.equal(await inspect('document.body.textContent.includes("Не удалось")'), false)
assert.equal((await state()).channels.music.desired, 'playing')
writeFileSync(backgroundPath, originalImage)
await command({ action: 'mode', mode: 8 })
await sceneReady(8)
await command({ action: 'stop', channel: 'music' })
const nativeCaptures = captures.filter((capture) => capture.id === output.id)
assert.ok(nativeCaptures.length > 0)
for (let index = 1; index < nativeCaptures.length; index++)
assert.ok(
nativeCaptures[index].time - nativeCaptures[index - 1].time >= 490,
'preview limited to 2 FPS',
)
// Фикстура проверяет таблицу и прокрутку в renderer, не создавая результаты в main.
await command({ action: 'mode', mode: 6 })
await sceneReady(6)
const realState = await state()
const resultFixture = {
...realState,
revision: realState.revision + 1000000,
scene: { ...realState.scene, revision: realState.scene.revision + 1 },
results: {
packageKey: offlinePackage.key,
capturedAt: '2026-10-17T10:00:00.000Z',
questions: [
{ code: 'К1', text: 'Первый вопрос' },
{ code: 'К2', text: 'Второй вопрос' },
],
teams: Array.from({ length: 50 }, (_, i) => ({
id: `fixture-${i}`,
name: `Детективы №${i + 1}`,
place: i + 1,
totalScore: 10,
actionsCount: 3,
answers: [{ questionCode: 'К1', score: 10 }],
})),
},
}
output.webContents.send('state', resultFixture)
await poll(
() => inspect('document.querySelectorAll("tbody tr").length === 50'),
'results table fixture',
)
assert.equal(
await inspect('document.querySelector("tbody tr td:nth-child(4)").textContent'),
'0',
)
const scroll = await inspect('document.querySelector(".results-scroll").scrollTop')
assert.ok(scroll > 0)
await delay(250)
const afterScroll = await inspect('document.querySelector(".results-scroll").scrollTop')
assert.ok(afterScroll < scroll)
output.webContents.send('state', {
...resultFixture,
revision: resultFixture.revision + 1,
results: structuredClone(resultFixture.results),
})
await delay(250)
assert.ok(
await inspect(`document.querySelector('.results-scroll').scrollTop < ${afterScroll}`),
'media state updates do not restart result scrolling',
)
const samples = (await state()).samples
const oldSession = (await state()).session
output.close()
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
output = await poll(() => BrowserWindow.getAllWindows().find((w) => w !== control), 'reopened')
await poll(async () => {
const s = await state()
return (
s.session !== oldSession &&
s.channels.main.status === 'stopped' &&
s.channels.music.status === 'stopped'
)
}, 'reopened prepared, stopped')
await checkDisplay(output)
assert.equal(
await inspect(
"Array.from(document.querySelectorAll('audio,video')).filter(m => !m.paused).length",
),
0,
)
await evaluate(
`window.screenBridge.command(${JSON.stringify({ session: oldSession, sequence: 9999999, action: 'play', channel: 'main' })})`,
)
assert.equal((await state()).channels.main.desired, 'stopped')
assert.equal(await evaluate('typeof window.screenBridge.lan'), 'undefined')
assert.equal((await state()).results, null)
await require('./online-smoke.cjs')({
evaluate,
inspect,
state,
command,
poll,
displayId: targetDisplay.id,
root,
})
await require('./venue-smoke.cjs')({
control,
evaluate,
state,
command,
poll,
targetDisplay,
offlinePackage,
powerBlocks,
})
console.log(
'SCREEN_SMOKE_PASS',
JSON.stringify({
range,
offlinePackage: {
name: offlinePackage.package.game.name,
files: offlinePackage.files.length,
},
display: { id: targetDisplay.id, synthetic: isSyntheticDisplay },
modes,
previewCaptures: nativeCaptures.length,
fixturePosition,
pausedAt,
samples,
electron: process.versions.electron,
}),
)
app.exit(0)
} catch (error) {
for (const window of BrowserWindow.getAllWindows()) {
try {
console.error(
'SMOKE_DIAGNOSTIC',
window.webContents.getURL(),
await window.webContents.executeJavaScript(
"(async () => ({ state: await window.screenBridge.state(), host: await window.screenBridge.host().catch(() => null), media: Array.from(document.querySelectorAll('audio,video')).map(m => ({channel:m.dataset.channel,src:m.src,paused:m.paused,ready:m.readyState,time:m.currentTime,duration:m.duration,error:m.error?.message})) }))()",
),
)
} catch {}
}
console.error('SCREEN_SMOKE_FAIL', error)
app.exit(1)
}
})
if (!manual)
setTimeout(() => {
console.error('Global timeout')
app.exit(1)
}, 90000)