up
This commit is contained in:
+216
-10
@@ -1,7 +1,7 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { app, BrowserWindow, screen } = require('electron')
|
||||
const { app, BrowserWindow, screen, nativeImage } = require('electron')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { mkdtempSync, readFileSync } = require('node:fs')
|
||||
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-')))
|
||||
@@ -11,7 +11,13 @@ let targetDisplay
|
||||
let isSyntheticDisplay = false
|
||||
const boundsBeforeFullscreen = new Map()
|
||||
const fullscreenEntered = new Set()
|
||||
const captures = []
|
||||
app.on('browser-window-created', (_event, window) => {
|
||||
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)
|
||||
@@ -74,17 +80,40 @@ const seeded = (async () => {
|
||||
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: {
|
||||
story: { introduction: { audio: 'intro.wav' }, conclusion: { video: 'ending.mp4' } },
|
||||
image: 'cover.png',
|
||||
story: {
|
||||
background: 'background.png',
|
||||
introduction: { audio: 'intro.wav' },
|
||||
conclusion: { video: 'ending.mp4' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plan: {},
|
||||
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: [
|
||||
@@ -102,14 +131,22 @@ const seeded = (async () => {
|
||||
const store = new PackageStore(join(app.getPath('userData'), 'packages'), {
|
||||
fetch: async (input) => {
|
||||
const url = String(input)
|
||||
const bytes = url.endsWith('intro.wav')
|
||||
? audio
|
||||
: url.endsWith('.mp4')
|
||||
? video
|
||||
: musicWav(url.endsWith('two.wav') ? 660 : 330)
|
||||
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('.mp4') ? 'video/mp4' : 'audio/wav',
|
||||
'Content-Type': url.endsWith('.png')
|
||||
? 'image/png'
|
||||
: url.endsWith('.mp4')
|
||||
? 'video/mp4'
|
||||
: 'audio/wav',
|
||||
'Content-Length': String(bytes.length),
|
||||
},
|
||||
})
|
||||
@@ -321,6 +358,172 @@ app.whenReady().then(async () => {
|
||||
"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 === 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")'))
|
||||
}
|
||||
const publishedText = (await state()).scene.text
|
||||
await evaluate(
|
||||
`(() => { const input = document.querySelector('#scene-text'); input.value = 'Черновик ещё не показан'; input.dispatchEvent(new Event('input', {bubbles:true})) })()`,
|
||||
)
|
||||
await delay(100)
|
||||
assert.equal((await state()).scene.text, publishedText)
|
||||
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()
|
||||
@@ -346,6 +549,7 @@ app.whenReady().then(async () => {
|
||||
)
|
||||
assert.equal((await state()).channels.main.desired, 'stopped')
|
||||
assert.equal(await evaluate('typeof window.screenBridge.lan'), 'undefined')
|
||||
assert.equal((await state()).results, null)
|
||||
console.log(
|
||||
'SCREEN_SMOKE_PASS',
|
||||
JSON.stringify({
|
||||
@@ -355,6 +559,8 @@ app.whenReady().then(async () => {
|
||||
files: offlinePackage.files.length,
|
||||
},
|
||||
display: { id: targetDisplay.id, synthetic: isSyntheticDisplay },
|
||||
modes,
|
||||
previewCaptures: nativeCaptures.length,
|
||||
fixturePosition,
|
||||
pausedAt,
|
||||
samples,
|
||||
|
||||
Reference in New Issue
Block a user