up
This commit is contained in:
@@ -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
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user