This commit is contained in:
2026-10-17 20:57:16 +07:00
parent 40456eebf1
commit 1d4028b183
13 changed files with 849 additions and 48 deletions
+172
View File
@@ -0,0 +1,172 @@
const assert = require('node:assert/strict')
const { app, BrowserWindow, screen, powerMonitor } = require('electron')
const { readFileSync } = require('node:fs')
const { join } = require('node:path')
module.exports = async function venueSmoke({
control,
evaluate,
state,
command,
poll,
targetDisplay,
offlinePackage,
powerBlocks,
}) {
const getDisplays = screen.getAllDisplays.bind(screen)
const getPrimary = screen.getPrimaryDisplay.bind(screen)
let removed = false
screen.getAllDisplays = () => getDisplays().filter((d) => !removed || d.id !== targetDisplay.id)
const host = () => evaluate('window.screenBridge.host()')
const currentOutput = () => BrowserWindow.getAllWindows().find((w) => w !== control)
async function open() {
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
await poll(async () => {
const s = await state()
return s.outputOpen && s.scene.status === 'ready' && s.channels.main.status === 'stopped'
}, 'venue open prepared')
assert.equal((await host()).sleepBlocked, true)
assert.equal(powerBlocks.active.size, 1)
assert.equal((await state()).timer.status, 'paused')
return currentOutput()
}
async function start() {
await command({ action: 'timer-set', label: 'Аварийная проверка', durationSeconds: 120 })
await command({ action: 'timer-start' })
await command({ action: 'play', channel: 'main' })
await command({ action: 'play', channel: 'music' })
}
async function stopped(label) {
await poll(async () => !(await state()).outputOpen, label)
const s = await state()
assert.equal(s.timer.status, 'paused')
assert.ok(s.timer.remainingMs > 0 && s.timer.remainingMs <= 120000)
assert.equal(s.channels.main.desired, 'stopped')
assert.equal(s.channels.music.desired, 'stopped')
assert.equal((await host()).sleepBlocked, false)
assert.equal(powerBlocks.active.size, 0)
assert.ok((await host()).notice.length > 0)
assert.ok(!control.isDestroyed() && control.isVisible())
return s
}
try {
await evaluate('window.screenBridge.closeOutput()')
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
// Первый пуск начинается с остановленного таймера; остальные — с аварийной паузы.
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
await poll(async () => (await state()).scene.status === 'ready', 'venue first open')
await start()
const old = currentOutput()
removed = true
screen.emit('display-removed', {}, targetDisplay)
const closed = await stopped('display removed')
screen.emit('display-removed', {}, targetDisplay)
assert.equal((await state()).session, closed.session)
removed = false
screen.emit('display-added', {}, targetDisplay)
assert.equal((await state()).outputOpen, false)
old.emit('ready-to-show')
old.emit('leave-full-screen')
assert.equal((await state()).outputOpen, false)
const remaining = closed.timer.remainingMs
let output = await open()
assert.equal((await state()).timer.remainingMs, remaining)
// Запоздавшие события старого окна не должны закрывать новое.
old.emit('closed')
assert.equal((await state()).outputOpen, true)
await start()
screen.emit('display-metrics-changed', {}, targetDisplay, ['workArea'])
assert.equal((await state()).outputOpen, true)
screen.emit('display-metrics-changed', {}, targetDisplay, ['bounds'])
await stopped('resolution changed')
for (const metric of ['scaleFactor', 'rotation']) {
await open()
await start()
screen.emit('display-metrics-changed', {}, targetDisplay, [metric])
await stopped(metric)
}
await open()
await start()
screen.getPrimaryDisplay = () => ({ ...targetDisplay, workArea: targetDisplay.bounds })
screen.emit('display-metrics-changed', {}, targetDisplay, ['workArea'])
await stopped('projector became primary')
assert.equal((await host()).preferredDisplayId, null)
screen.getPrimaryDisplay = getPrimary
screen.emit('display-added', {}, getPrimary())
const bounds = control.getNormalBounds()
const work = getPrimary().workArea
assert.ok(bounds.x >= work.x && bounds.y >= work.y)
assert.ok(bounds.x + bounds.width <= work.x + work.width)
assert.ok(bounds.y + bounds.height <= work.y + work.height)
output = await open()
await start()
powerMonitor.emit('suspend')
await stopped('system suspend')
await assert.rejects(evaluate(`window.screenBridge.show(${targetDisplay.id})`), /пробуждения/)
powerMonitor.emit('resume')
assert.equal((await state()).outputOpen, false)
output = await open()
await start()
// События fullscreen синтетические; физический переход проверяется отдельно.
output.emit('enter-full-screen')
output.emit('leave-full-screen')
await stopped('fullscreen exited')
output = await open()
await start()
output.webContents.emit('did-fail-load', {}, -2, 'test subresource', '', false)
assert.equal((await state()).outputOpen, true)
assert.notEqual(
output.webContents.getOSProcessId(),
control.webContents.getOSProcessId(),
'output renderer must be isolated from control',
)
const rendererPid = output.webContents.getOSProcessId()
assert.ok(rendererPid > 0 && rendererPid !== process.pid)
// SIGKILL даёт настоящий render-process-gone без зависания сборщика crash dump.
process.kill(rendererPid, 'SIGKILL')
await stopped('renderer crashed')
output = await open()
await start()
output.webContents.emit('did-fail-load', {}, -2, 'test main document', '', true)
await stopped('main document failed')
output = await open()
await start()
output.emit('unresponsive')
await stopped('window hung')
// Обычная кнопка закрытия не меняет прежнюю политику таймера.
await open()
await start()
await evaluate('window.screenBridge.closeOutput()')
assert.equal((await state()).timer.status, 'running')
powerMonitor.emit('suspend')
assert.equal((await state()).timer.status, 'paused')
powerMonitor.emit('resume')
await open()
control.setBounds({
x: work.x + 30,
y: work.y + 30,
width: Math.min(900, work.width - 30),
height: Math.min(650, work.height - 30),
})
app.emit('before-quit', {})
assert.equal(powerBlocks.active.size, 0)
assert.equal((await host()).sleepBlocked, false)
const saved = JSON.parse(readFileSync(join(app.getPath('userData'), 'windows.json'), 'utf8'))
assert.equal(saved.display.id, targetDisplay.id)
assert.equal(saved.display.label, targetDisplay.label)
assert.deepEqual(saved.control.bounds, control.getNormalBounds())
assert.equal(powerBlocks.starts, powerBlocks.stops)
console.log(
'SCREEN_VENUE_SMOKE_PASS',
JSON.stringify({
blockerStarts: powerBlocks.starts,
blockerStops: powerBlocks.stops,
rendererCrash: true,
incidents: 10,
}),
)
} finally {
screen.getAllDisplays = getDisplays
screen.getPrimaryDisplay = getPrimary
}
}