up
This commit is contained in:
+268
-130
@@ -1,13 +1,11 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { app, BrowserWindow, screen } = require('electron')
|
||||
const root = require('node:path').resolve(__dirname, '..')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { mkdtempSync, readFileSync } = require('node:fs')
|
||||
const root = resolve(__dirname, '..')
|
||||
app.setAppPath(root)
|
||||
app.setPath(
|
||||
'userData',
|
||||
require('node:fs').mkdtempSync(
|
||||
require('node:path').join(require('node:os').tmpdir(), 'screen-smoke-'),
|
||||
),
|
||||
)
|
||||
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
|
||||
@@ -31,6 +29,11 @@ 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 = {
|
||||
@@ -43,27 +46,74 @@ app.whenReady().then(() => {
|
||||
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 bytes = require('node:fs').readFileSync(root + '/fixtures/audio.wav')
|
||||
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: 'audio.wav' } } },
|
||||
scenario: {
|
||||
story: { introduction: { audio: 'intro.wav' }, conclusion: { video: 'ending.mp4' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
plan: {},
|
||||
music: {},
|
||||
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(require('node:path').join(app.getPath('userData'), 'packages'), {
|
||||
fetch: async () =>
|
||||
new Response(bytes, {
|
||||
headers: { 'Content-Type': 'audio/wav', 'Content-Length': String(bytes.length) },
|
||||
}),
|
||||
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)
|
||||
@@ -72,7 +122,7 @@ seeded.catch((error) => {
|
||||
console.error('PACKAGE_SEED_FAIL', error)
|
||||
app.exit(1)
|
||||
})
|
||||
// Регистрация протокола выполняется до ready; открытие хранилища ждёт seed.
|
||||
// Регистрация протокола до ready; новый экземпляр хранилища открывается после seed.
|
||||
const whenReady = app.whenReady.bind(app)
|
||||
app.whenReady = () => whenReady().then(() => seeded)
|
||||
require(root + '/dist-electron/electron/main.js')
|
||||
@@ -87,161 +137,249 @@ async function poll(check, 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 control.webContents.executeJavaScript(
|
||||
'!!window.screenBridge && !!document.querySelector("h1")',
|
||||
)),
|
||||
(await evaluate('!!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)
|
||||
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})`)
|
||||
const output = await poll(
|
||||
let 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 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')
|
||||
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)
|
||||
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 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',
|
||||
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',
|
||||
)
|
||||
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})`,
|
||||
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 evaluate("document.body.textContent.includes('Телефон по локальной сети')"),
|
||||
false,
|
||||
await inspect(
|
||||
"Array.from(document.querySelectorAll('audio,video')).filter(m => !m.paused).length",
|
||||
),
|
||||
2,
|
||||
)
|
||||
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')
|
||||
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})`)
|
||||
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)
|
||||
}
|
||||
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 reopened.webContents.executeJavaScript('document.querySelector("video").paused'),
|
||||
true,
|
||||
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: offline,
|
||||
display: {
|
||||
id: targetDisplay.id,
|
||||
bounds: targetDisplay.bounds,
|
||||
synthetic: isSyntheticDisplay,
|
||||
offlinePackage: {
|
||||
name: offlinePackage.package.game.name,
|
||||
files: offlinePackage.files.length,
|
||||
},
|
||||
seek: position,
|
||||
samples: (await evaluate('window.screenBridge.state()')).samples,
|
||||
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)
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
console.error('Global timeout')
|
||||
app.exit(1)
|
||||
}, 45000)
|
||||
if (!manual)
|
||||
setTimeout(() => {
|
||||
console.error('Global timeout')
|
||||
app.exit(1)
|
||||
}, 90000)
|
||||
|
||||
Reference in New Issue
Block a user