const assert = require('node:assert/strict') const { createServer } = require('node:http') const { readFileSync } = require('node:fs') const { join } = require('node:path') module.exports = async function onlineSmoke({ evaluate, inspect, state, command, poll, displayId, root, }) { let version = 1 let offline = false let polls = 0 let currentCommand = 'SCREEN_COMMAND_PLAY' const requests = [] const server = createServer(async (req, res) => { const chunks = [] for await (const chunk of req) chunks.push(chunk) const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString()) : {} const path = req.url requests.push(path) const send = (value, status = 200) => { res.writeHead(status, { 'Content-Type': 'application/json' }) res.end(JSON.stringify(value)) } if (path === '/api/auth/login') { assert.equal(body.email, 'organizer@example.test') return send({ accessToken: 'TEST-ACCESS', refreshToken: 'TEST-REFRESH' }) } if (path === '/api/users/me') return send({ user: { roles: ['organizer'] } }) if (path === '/api/games') return send({ games: [{ ID: 6, name: 'HTTP smoke', status: 'finish' }] }) if (path === '/api/games/6') return send({ game: { ID: 6, name: 'HTTP smoke', scenario: { story: { introduction: { audio: 'intro.wav' } } }, }, }) if (path === '/api/games/6/screen-plan') return send({ plan: { steps: [{ mode: 2 }] } }) if (path === '/api/games/6/music') return send({}) if (path === '/api/files/intro.wav') { assert.equal(req.headers.authorization, undefined) res.writeHead(200, { 'Content-Type': 'audio/wav' }) return res.end(readFileSync(join(root, 'fixtures/audio.wav'))) } if (path === '/api/screens') return send({ screenID: '67', accessToken: 'TEST-SCREEN' }) if (path === '/api/screens/67/bind') return send({}) if (path === '/api/screens/state') { polls++ assert.equal(req.headers.authorization, undefined) assert.equal(body.accessToken, 'TEST-SCREEN') if (offline) return send({}, 503) return send({ state: { gameID: 6, mode: 'SCREEN_MODE_INTRO', soundEnabled: true, volume: 80, stateVersion: String(version), command: currentCommand, }, }) } if (path === '/api/screens/results') return send({ questions: [{ code: 'A', text: 'Вопрос' }], teams: [ { team: { ID: 't', name: 'Детективы', password: 'PRIVATE' }, place: 1, totalScore: 3, actionsCount: 1, answers: [{ questionCode: 'A', score: 3 }], }, ], }) send({}, 404) }) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const url = `http://127.0.0.1:${server.address().port}` const act = (intent) => evaluate(`window.screenBridge.onlineAction(${JSON.stringify(intent)})`) try { await evaluate('window.screenBridge.closeOutput()') // Вход через настоящую форму пульта, загрузка через защищённый IPC. await poll(() => evaluate('!!document.querySelector("#server")'), 'login form') await evaluate(`(() => { for (const [id, value] of Object.entries(${JSON.stringify({ server: url, email: 'organizer@example.test', password: 'test-password' })})) { const element = document.querySelector('#' + id) element.value = value; element.dispatchEvent(new Event('input', { bubbles: true })) } })()`) await evaluate( "Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Войти').click()", ) await poll( async () => (await evaluate('window.screenBridge.onlineState()')).authenticated, 'HTTP login', ) await poll( async () => !(await evaluate('window.screenBridge.onlineState()')).busy, 'login complete', ) await act({ action: 'download', gameId: 6 }) const packages = await evaluate('window.screenBridge.packages()') const pkg = packages.find((p) => p.name === 'HTTP smoke') assert.ok(pkg) await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(pkg.key)})`) await act({ action: 'connect' }) await evaluate(`window.screenBridge.show(${displayId})`) await poll( async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'connected', 'HTTP baseline', ) assert.equal((await state()).channels.main.desired, 'stopped') version++ await poll( async () => (await state()).channels.main.status === 'playing', 'server play -> local audio', ) assert.match( await inspect('document.querySelector("[data-channel=main-audio]").src'), /^screen-app:/, ) await assert.rejects(act({ action: 'download', gameId: 6 }), /Закройте показ/) await command({ action: 'pause', channel: 'main' }) const pausePolls = polls await poll(() => polls > pausePolls + 1, 'duplicate state polled') assert.equal((await state()).channels.main.desired, 'paused') offline = true await poll( async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'offline', 'offline indication', ) await command({ action: 'play', channel: 'main' }) offline = false currentCommand = 'SCREEN_COMMAND_STOP' version++ await poll( async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'connected', 'reconnect baseline', ) assert.equal((await state()).channels.main.desired, 'playing') version++ await poll(async () => (await state()).channels.main.status === 'stopped', 'new remote stop') const { powerMonitor } = require('electron') powerMonitor.emit('suspend') assert.equal((await state()).outputOpen, false) powerMonitor.emit('resume') currentCommand = 'SCREEN_COMMAND_PLAY' version++ const beforeReopenPolls = polls await evaluate(`window.screenBridge.show(${displayId})`) await poll(() => polls > beforeReopenPolls, 'post-incident baseline') assert.equal((await state()).channels.main.desired, 'stopped') version++ await poll( async () => (await state()).channels.main.status === 'playing', 'new command after incident', ) await act({ action: 'results' }) assert.equal((await state()).results.teams[0].name, 'Детективы') assert.equal(JSON.stringify(await state()).includes('PRIVATE'), false) for (const method of ['onlineState()', "onlineAction({action:'logout'})", 'closeOutput()']) { assert.equal( await inspect(`window.screenBridge.${method}.then(() => false, () => true)`), true, ) } await act({ action: 'logout' }) assert.equal((await evaluate('window.screenBridge.onlineState()')).authenticated, false) console.log('SCREEN_ONLINE_SMOKE_PASS', JSON.stringify({ polls, requests: requests.length })) } finally { server.closeAllConnections() await new Promise((resolve) => server.close(resolve)) } }