This commit is contained in:
2026-10-18 18:00:12 +07:00
parent e881f10857
commit a3317dbf7a
11 changed files with 834 additions and 445 deletions
+77 -1
View File
@@ -331,6 +331,38 @@ async function fixture(t: TestContext, secure = true) {
prepare,
}
}
test('подтверждение игры загружает материалы без подключения экрана и сохраняет выбор', async (t) => {
const f = await fixture(t)
await f.login()
await f.online.act({ action: 'select-game', gameId: 7 })
const selected = f.online.snapshot()
assert.equal(selected.currentGameId, 7)
assert.equal(selected.screenId, null)
assert.equal(selected.connection, 'disabled')
assert.ok(selected.materials.length > 0)
assert.ok(selected.materials.every((m) => m.status === 'ready'))
assert.equal(f.model.bindings.length, 0)
assert.ok(!f.requests.some((r) => r.path === '/api/screens'))
const saved = JSON.parse(await readFile(join(f.directory, 'connection', 'selection.json'), 'utf8'))
assert.equal(saved.key, f.controller.state.catalog.key)
await f.online.act({ action: 'connect' })
assert.equal(f.online.snapshot().screenId, '12')
assert.equal(f.model.bindings.at(-1)!.gameID, 7)
})
test('ошибка подготовки и открытый показ не подтверждают выбор игры', async (t) => {
const f = await fixture(t)
await f.login()
f.controller.output(true)
await assert.rejects(f.online.act({ action: 'select-game', gameId: 7 }), /Закройте показ/)
f.controller.output(false)
f.model.failMedia = true
await assert.rejects(f.online.act({ action: 'select-game', gameId: 7 }))
assert.equal(f.controller.state.catalog.key, null)
assert.equal(f.online.snapshot().currentGameId, null)
assert.equal(f.model.bindings.length, 0)
})
test('чистый профиль предлагает адрес сайта по умолчанию', async (t) => {
const f = await fixture(t)
assert.equal(f.online.snapshot().server, 'https://evening-detective.crabs-games.art')
@@ -446,6 +478,50 @@ for (const gameId of [7, 8]) {
})
}
for (const status of [200, 404]) {
test(`отключение отвязывает экран, сохраняет игру и переживает перезапуск: ${status}`, async (t) => {
const f = await fixture(t)
await f.prepare()
const key = f.controller.state.catalog.key
const materials = f.online.snapshot().materials
f.model.unbindError = status === 200 ? 0 : status
await f.online.act({ action: 'disconnect' })
assert.equal(f.requests.at(-1)!.path, '/api/screens/12/unbind')
assert.equal(f.online.snapshot().screenId, null)
assert.equal(f.online.snapshot().connection, 'disabled')
assert.equal(f.controller.state.catalog.key, key)
assert.deepEqual(f.online.snapshot().materials, materials)
const saved = JSON.parse(f.secrets.decrypt(await readFile(join(f.directory, 'connection', 'credentials.bin'))))
assert.equal(saved.screen, null)
const restored = new Online(join(f.directory, 'connection'), f.store, new Controller(() => {}), f.secrets, f.fetchAPI)
t.after(() => restored.close())
await restored.initialize()
assert.equal(restored.snapshot().screenId, null)
assert.equal(restored.snapshot().currentGameId, 7)
const count = f.requests.length
await f.online.act({ action: 'disconnect' })
assert.equal(f.requests.length, count)
})
}
for (const failure of ['server', 'network'] as const) {
test(`ошибка отключения сохраняет сессию для повтора: ${failure}`, async (t) => {
const f = await fixture(t)
await f.prepare()
const saved = await readFile(join(f.directory, 'connection', 'credentials.bin'))
f.model.unbindError = failure === 'server' ? 500 : 0
f.model.offline = failure === 'network'
await assert.rejects(f.online.act({ action: 'disconnect' }))
assert.equal(f.online.snapshot().screenId, '12')
assert.equal(f.online.snapshot().connection, 'waiting')
assert.deepEqual(await readFile(join(f.directory, 'connection', 'credentials.bin')), saved)
f.model.unbindError = 0
f.model.offline = false
await f.online.act({ action: 'disconnect' })
assert.equal(f.online.snapshot().screenId, null)
})
}
test('ошибка отвязки сохраняет прежнюю сессию и не создаёт новую', async (t) => {
const f = await fixture(t)
await f.prepare()
@@ -1001,7 +1077,7 @@ test('назначение звука подтверждается сервер
const unbindsBeforeDisconnect = f.requests.filter((r) => r.path.endsWith('/unbind')).length
await f.online.act({ action: 'disconnect' })
assert.equal(f.online.state.soundEnabled, null)
assert.equal(f.requests.filter((r) => r.path.endsWith('/unbind')).length, unbindsBeforeDisconnect)
assert.equal(f.requests.filter((r) => r.path.endsWith('/unbind')).length, unbindsBeforeDisconnect + 1)
})
test('план: сохранение и удаление на сервере, восстановление копии офлайн, ошибки валидации и доступа', async (t) => {
+18 -2
View File
@@ -1171,6 +1171,7 @@ export class Online {
login: 'Вход на сервер…',
logout: 'Выход из аккаунта…',
games: 'Загрузка списка игр…',
'select-game': 'Подготовка выбранной игры…',
connect: 'Подключение игры…',
disconnect: 'Отключение игры…',
download: 'Загрузка материалов игры…',
@@ -1278,10 +1279,13 @@ export class Online {
return
case 'games':
return this.loadGames()
case 'select-game':
case 'download':
case 'check': {
const gameId = integer(data.gameId, 1)
const key = packageKey(this.state.server, gameId)
if (action === 'select-game' && this.controller.state.outputOpen)
throw new Error('Закройте показ перед выбором игры')
if (
action === 'download' &&
key === this.controller.state.catalog.key &&
@@ -1289,7 +1293,7 @@ export class Online {
)
throw new Error('Закройте показ перед обновлением выбранной игры')
if (!this.credentials) throw new ApiError(401)
this.downloadKey = action === 'download' ? key : null
this.downloadKey = action === 'check' ? null : key
this.cancellation = new AbortController()
this.state.canCancel = true
try {
@@ -1324,7 +1328,8 @@ export class Online {
},
})
this.state.packagesRevision++
if (key === this.controller.state.catalog.key) {
if (action === 'select-game' || key === this.controller.state.catalog.key) {
if (action === 'select-game') this.detach()
this.controller.selectPackage(pkg)
this.plan = {
steps: structuredClone(this.controller.state.catalog.steps),
@@ -1335,6 +1340,10 @@ export class Online {
await this.atomic(`plan-${key}.json`, this.cachedPlan)
this.previous = null
await this.restoreResults(key)
if (action === 'select-game') {
await this.saveSelection()
await this.persistSecrets()
}
}
this.state.notice = 'Материалы проверены и готовы к показу'
} finally {
@@ -1406,6 +1415,13 @@ export class Online {
return
}
case 'disconnect':
if (this.screen) {
try {
await this.client().UnbindScreen({ screenID: this.screen.id })
} catch (error) {
if (!(error instanceof ApiError && error.status === 404)) throw error
}
}
this.detach()
await this.persistSecrets()
return