248 lines
9.6 KiB
JavaScript
248 lines
9.6 KiB
JavaScript
const assert = require('node:assert/strict')
|
||
const { app, BrowserWindow, screen } = require('electron')
|
||
const root = require('node:path').resolve(__dirname, '..')
|
||
app.setAppPath(root)
|
||
app.setPath(
|
||
'userData',
|
||
require('node:fs').mkdtempSync(
|
||
require('node:path').join(require('node:os').tmpdir(), 'screen-smoke-'),
|
||
),
|
||
)
|
||
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) {
|
||
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]
|
||
}
|
||
})
|
||
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 bytes = require('node:fs').readFileSync(root + '/fixtures/audio.wav')
|
||
const pkg = buildScreenPackage('https://offline.invalid', 7, {
|
||
game: {
|
||
game: {
|
||
ID: 7,
|
||
name: 'Offline smoke',
|
||
scenario: { story: { introduction: { audio: 'audio.wav' } } },
|
||
},
|
||
},
|
||
plan: {},
|
||
music: {},
|
||
})
|
||
const store = new PackageStore(require('node:path').join(app.getPath('userData'), 'packages'), {
|
||
fetch: async () =>
|
||
new Response(bytes, {
|
||
headers: { 'Content-Type': '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 {
|
||
const control = await poll(() => BrowserWindow.getAllWindows()[0], 'control')
|
||
control.webContents.on('console-message', (_event, details) =>
|
||
console.log('renderer:', details.message),
|
||
)
|
||
await poll(
|
||
async () =>
|
||
!control.webContents.isLoading() &&
|
||
(await control.webContents.executeJavaScript(
|
||
'!!window.screenBridge && !!document.querySelector("h1")',
|
||
)),
|
||
'control UI',
|
||
)
|
||
const evaluate = (s) => control.webContents.executeJavaScript(s)
|
||
await seeded
|
||
const asset = offlinePackage.files[0]
|
||
const localURL = `screen-app://app/packages/${offlinePackage.key}/${asset.id}`
|
||
const offline = await evaluate(`(async () => {
|
||
const response = await fetch(${JSON.stringify(localURL)}, { headers: { Range: 'bytes=0-43' } })
|
||
const size = (await response.arrayBuffer()).byteLength
|
||
const audio = new Audio(${JSON.stringify(localURL)})
|
||
await new Promise((resolve, reject) => { audio.onloadedmetadata = resolve; audio.onerror = () => reject(new Error('Offline audio failed')) })
|
||
await audio.play()
|
||
const playing = !audio.paused
|
||
const duration = audio.duration
|
||
audio.pause()
|
||
return { status: response.status, size, duration, playing }
|
||
})()`)
|
||
assert.equal(offline.status, 206)
|
||
assert.equal(offline.size, 44)
|
||
assert.ok(offline.duration > 0)
|
||
assert.equal(offline.playing, true)
|
||
assert.equal(await evaluate('typeof window.require'), 'undefined')
|
||
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
|
||
const output = await poll(
|
||
() => BrowserWindow.getAllWindows().find((w) => w !== control),
|
||
'output',
|
||
)
|
||
output.webContents.on('console-message', (_event, details) =>
|
||
console.log('output:', details.message),
|
||
)
|
||
await poll(
|
||
async () =>
|
||
!output.webContents.isLoading() &&
|
||
(await output.webContents.executeJavaScript(
|
||
'document.querySelector("video")?.readyState >= 1',
|
||
)),
|
||
'metadata',
|
||
)
|
||
if (process.platform === 'linux')
|
||
assert.equal(app.commandLine.getSwitchValue('ozone-platform'), 'x11')
|
||
await poll(() => boundsBeforeFullscreen.has(output.id), 'output fullscreen requested')
|
||
assert.deepEqual(boundsBeforeFullscreen.get(output.id), targetDisplay.bounds)
|
||
if (!isSyntheticDisplay) {
|
||
await poll(() => fullscreenEntered.has(output.id), 'output entered fullscreen')
|
||
assert.ok(output.isFullScreen())
|
||
assert.equal(screen.getDisplayMatching(output.getBounds()).id, targetDisplay.id)
|
||
}
|
||
const range = await output.webContents.executeJavaScript(
|
||
`fetch('screen-app://app/media/video', {headers: {Range: 'bytes=100-199'}}).then(async r => ({status:r.status, size:(await r.arrayBuffer()).byteLength, range:r.headers.get('content-range')}))`,
|
||
)
|
||
assert.equal(range.status, 206)
|
||
assert.equal(range.size, 100)
|
||
const command = async (id, action, media = 'audio', position = 0) => {
|
||
await evaluate(
|
||
`window.screenBridge.command(${JSON.stringify({ id, action, media, position })})`,
|
||
)
|
||
return poll(async () => {
|
||
const s = await evaluate('window.screenBridge.state()')
|
||
if (s.status === 'error') throw new Error(s.error)
|
||
return s.status !== 'pending' && s.command.id === id && s
|
||
}, id)
|
||
}
|
||
await evaluate(
|
||
`Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).click()`,
|
||
)
|
||
await poll(async () => {
|
||
const state = await evaluate('window.screenBridge.state()')
|
||
if (state.status === 'error') throw new Error(state.error)
|
||
return state.status === 'playing'
|
||
}, 'computer UI play')
|
||
assert.equal((await command('audio-play', 'play')).status, 'playing')
|
||
assert.equal((await command('audio-pause', 'pause')).status, 'paused')
|
||
assert.equal((await command('audio-stop', 'stop')).status, 'stopped')
|
||
assert.equal((await command('video-play', 'play', 'video')).status, 'playing')
|
||
await command('video-seek', 'seek', 'video', 30)
|
||
const position = await output.webContents.executeJavaScript(
|
||
'document.querySelector("video").currentTime',
|
||
)
|
||
assert.ok(position >= 29.9 && position < 35, 'video seek actual position')
|
||
await command('video-pause', 'pause', 'video')
|
||
assert.equal(
|
||
await output.webContents.executeJavaScript('document.querySelector("audio").paused'),
|
||
true,
|
||
)
|
||
assert.equal(
|
||
await output.webContents.executeJavaScript('document.querySelector("video").paused'),
|
||
true,
|
||
)
|
||
await assert.rejects(
|
||
output.webContents.executeJavaScript(
|
||
`window.screenBridge.command({id:'forbidden', action:'play', media:'audio', position:0})`,
|
||
),
|
||
)
|
||
assert.equal(
|
||
await evaluate("document.body.textContent.includes('Телефон по локальной сети')"),
|
||
false,
|
||
)
|
||
assert.deepEqual(Object.keys(await evaluate('window.screenBridge.host()')), ['displays'])
|
||
assert.equal(await evaluate('typeof window.screenBridge.lan'), 'undefined')
|
||
assert.equal(await evaluate('typeof window.screenBridge.approve'), 'undefined')
|
||
output.close()
|
||
await evaluate(`window.screenBridge.show(${targetDisplay.id})`)
|
||
const reopened = await poll(
|
||
() => BrowserWindow.getAllWindows().find((w) => w !== control),
|
||
'reopened output',
|
||
)
|
||
await poll(
|
||
async () =>
|
||
!reopened.webContents.isLoading() &&
|
||
(await reopened.webContents.executeJavaScript('!!document.querySelector("video")')),
|
||
'reopened ready',
|
||
)
|
||
await poll(() => boundsBeforeFullscreen.has(reopened.id), 'reopened fullscreen requested')
|
||
assert.deepEqual(boundsBeforeFullscreen.get(reopened.id), targetDisplay.bounds)
|
||
if (!isSyntheticDisplay) {
|
||
await poll(() => fullscreenEntered.has(reopened.id), 'reopened entered fullscreen')
|
||
assert.ok(reopened.isFullScreen())
|
||
assert.equal(screen.getDisplayMatching(reopened.getBounds()).id, targetDisplay.id)
|
||
}
|
||
assert.equal(
|
||
await reopened.webContents.executeJavaScript('document.querySelector("video").paused'),
|
||
true,
|
||
)
|
||
console.log(
|
||
'SCREEN_SMOKE_PASS',
|
||
JSON.stringify({
|
||
range,
|
||
offlinePackage: offline,
|
||
display: {
|
||
id: targetDisplay.id,
|
||
bounds: targetDisplay.bounds,
|
||
synthetic: isSyntheticDisplay,
|
||
},
|
||
seek: position,
|
||
samples: (await evaluate('window.screenBridge.state()')).samples,
|
||
electron: process.versions.electron,
|
||
}),
|
||
)
|
||
app.exit(0)
|
||
} catch (error) {
|
||
console.error('SCREEN_SMOKE_FAIL', error)
|
||
app.exit(1)
|
||
}
|
||
})
|
||
setTimeout(() => {
|
||
console.error('Global timeout')
|
||
app.exit(1)
|
||
}, 45000)
|