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
+15
View File
@@ -148,6 +148,21 @@ module.exports = async function onlineSmoke({
assert.equal((await state()).channels.main.desired, 'playing')
version++
await poll(async () => (await state()).channels.main.status === 'stopped', 'new remote stop')
const { powerMonitor } = require('electron')
powerMonitor.emit('suspend')
assert.equal((await state()).outputOpen, false)
powerMonitor.emit('resume')
currentCommand = 'SCREEN_COMMAND_PLAY'
version++
const beforeReopenPolls = polls
await evaluate(`window.screenBridge.show(${displayId})`)
await poll(() => polls > beforeReopenPolls, 'post-incident baseline')
assert.equal((await state()).channels.main.desired, 'stopped')
version++
await poll(
async () => (await state()).channels.main.status === 'playing',
'new command after incident',
)
await act({ action: 'results' })
assert.equal((await state()).results.teams[0].name, 'Детективы')
assert.equal(JSON.stringify(await state()).includes('PRIVATE'), false)
+37 -2
View File
@@ -1,5 +1,5 @@
const assert = require('node:assert/strict')
const { app, BrowserWindow, screen, nativeImage } = require('electron')
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, '..')
@@ -11,8 +11,26 @@ 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() })
@@ -28,6 +46,13 @@ app.on('browser-window-created', (_event, window) => {
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)
}
})
@@ -562,6 +587,16 @@ app.whenReady().then(async () => {
displayId: targetDisplay.id,
root,
})
await require('./venue-smoke.cjs')({
control,
evaluate,
state,
command,
poll,
targetDisplay,
offlinePackage,
powerBlocks,
})
console.log(
'SCREEN_SMOKE_PASS',
JSON.stringify({
@@ -587,7 +622,7 @@ app.whenReady().then(async () => {
'SMOKE_DIAGNOSTIC',
window.webContents.getURL(),
await window.webContents.executeJavaScript(
"(async () => ({ state: await window.screenBridge.state(), 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})) }))()",
"(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 {}
+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
}
}