const assert = require('node:assert/strict') const { app, BrowserWindow, screen } = require('electron') const { join, resolve } = require('node:path') const { mkdtempSync, readFileSync } = 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() app.on('browser-window-created', (_event, window) => { 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) 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 pkg = buildScreenPackage('https://offline.invalid', 7, { game: { game: { ID: 7, name: 'Offline smoke', scenario: { story: { introduction: { audio: 'intro.wav' }, conclusion: { video: 'ending.mp4' } }, }, }, }, plan: {}, 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('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-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') 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) { 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) => output.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 evaluate( `Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).click()`, ) await poll(async () => (await state()).channels.main.status === 'playing', 'UI 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' }) await evaluate("document.querySelector('.n-base-selection').click()") await poll( () => evaluate(`(() => { const option = Array.from(document.querySelectorAll('.n-base-select-option')).find(el => el.textContent.includes('Offline smoke')) if (!option) return false option.click(); return true })()`), 'UI package choice', ) 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/'))", ), ) 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') console.log( 'SCREEN_SMOKE_PASS', JSON.stringify({ range, offlinePackage: { name: offlinePackage.package.game.name, files: offlinePackage.files.length, }, display: { id: targetDisplay.id, synthetic: isSyntheticDisplay }, 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(), 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)