This commit is contained in:
2026-10-17 18:14:31 +07:00
parent 1c42ae2a75
commit 701bca234e
31 changed files with 9774 additions and 21 deletions
+48
View File
@@ -0,0 +1,48 @@
const { mkdirSync } = require('node:fs')
const { spawnSync } = require('node:child_process')
mkdirSync('fixtures', { recursive: true })
const jobs = [
[
'-f',
'lavfi',
'-i',
'sine=frequency=1000:sample_rate=48000:duration=60',
'-af',
'volume=0.1',
'-c:a',
'pcm_s16le',
'fixtures/audio.wav',
],
[
'-f',
'lavfi',
'-i',
'testsrc2=size=1280x720:rate=30:duration=60',
'-f',
'lavfi',
'-i',
'sine=frequency=1000:sample_rate=48000:duration=60',
'-af',
'volume=0.1',
'-c:v',
'libx264',
'-preset',
'fast',
'-pix_fmt',
'yuv420p',
'-c:a',
'aac',
'-movflags',
'+faststart',
'-shortest',
'fixtures/video.mp4',
],
]
for (const args of jobs) {
const result = spawnSync('ffmpeg', ['-hide_banner', '-loglevel', 'error', '-y', ...args], {
stdio: 'inherit',
})
if (result.error)
console.error('Установите ffmpeg с libx264 и добавьте в PATH:', result.error.message)
if (result.error || result.status !== 0) process.exit(1)
}
+191
View File
@@ -0,0 +1,191 @@
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))
app.whenReady().then(() => {
const original = screen.getAllDisplays.bind(screen)
screen.getAllDisplays = () => [
...original(),
{
...original()[0],
id: 424242,
label: 'Synthetic test display',
bounds: { x: 1280, y: 0, width: 1280, height: 720 },
size: { width: 1280, height: 720 },
},
]
})
require(root + '/dist-electron/electron/main.js')
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)
assert.equal(await evaluate('typeof window.require'), 'undefined')
await evaluate('window.screenBridge.show(424242)')
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',
)
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)
}
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})`,
),
)
const network = (await evaluate('window.screenBridge.host()')).lan[0]
assert.ok(network, 'LAN interface available for browser smoke')
await evaluate(`window.screenBridge.lan(${JSON.stringify(network.address)})`)
const host = await evaluate('window.screenBridge.host()')
const phone = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
backgroundThrottling: false,
partition: 'screen-smoke-phone',
},
})
assert.equal((await fetch(host.url)).status, 200)
await phone.webContents.session.setProxy({ mode: 'direct' })
await phone.loadURL(host.url)
const phoneEval = (s) => phone.webContents.executeJavaScript(s)
await poll(async () => await phoneEval('!!document.querySelector("input")'), 'phone UI')
assert.equal(await phoneEval('typeof window.screenBridge'), 'undefined')
await phoneEval(
`(() => {const input = document.querySelector('input'); input.value = ${JSON.stringify(host.code)}; input.dispatchEvent(new Event('input', {bubbles:true}));})()`,
)
await delay(100)
await phoneEval(
`Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Подключиться')).click()`,
)
const pending = await poll(
async () => (await evaluate('window.screenBridge.host()')).pending[0],
'pair request from phone UI',
)
await evaluate(`window.screenBridge.approve(${JSON.stringify(pending)})`)
await poll(
async () =>
await phoneEval(
`!Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).disabled`,
),
'phone connected',
)
await phoneEval(
`Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).click()`,
)
await poll(async () => {
const state = await evaluate('window.screenBridge.state()')
return state.status === 'playing' && state.samples.some((s) => s.origin === 'phone')
}, 'phone play')
await command('computer-after-phone', 'pause')
await evaluate('window.screenBridge.lan(null)')
await poll(
async () => await phoneEval(`document.body.textContent.includes('Нет связи')`),
'phone revoked',
)
phone.close()
output.close()
await evaluate('window.screenBridge.show(424242)')
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',
)
assert.equal(
await reopened.webContents.executeJavaScript('document.querySelector("video").paused'),
true,
)
console.log(
'SCREEN_SMOKE_PASS',
JSON.stringify({
range,
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)