up
This commit is contained in:
@@ -418,3 +418,28 @@
|
|||||||
- Границы: изменения только evening_detective_screen. Сервер и сгенерированный
|
- Границы: изменения только evening_detective_screen. Сервер и сгенерированный
|
||||||
клиент не менялись. Production-ошибка не воспроизводилась; установочные
|
клиент не менялись. Production-ошибка не воспроизводилась; установочные
|
||||||
архивы не пересобирались, commit/push не выполнялись.
|
архивы не пересобирались, commit/push не выполнялись.
|
||||||
|
|
||||||
|
## SCREEN-021 — Подключение игры без обязательного story
|
||||||
|
|
||||||
|
- Статус: `DONE`.
|
||||||
|
- Дата: 2026-10-18.
|
||||||
|
- Результат: scenario и story могут отсутствовать или быть null. Игра и её ID
|
||||||
|
остаются обязательными; неверные типы присутствующих полей дают подробную ошибку.
|
||||||
|
После получения GetScreenState обложка, фон, вступление и видео берутся из его
|
||||||
|
ссылок, включая пустые значения как отсутствие файла. До получения состояния
|
||||||
|
используются доступные материалы GetGame; полный сценарий отдельно не запрашивается.
|
||||||
|
- Ссылки включаются в существующее описание и автоматически скачиваются локально.
|
||||||
|
Сохранены защита от устаревших загрузок, прежний показ до готовности замены,
|
||||||
|
чтение кеша после перезапуска и формат пакета. Явно отсутствующий фон сохраняется
|
||||||
|
при наличии обложки, без восстановления неявного fallback при чтении манифеста.
|
||||||
|
Без медиа работают текст, таймер и музыка; вступление/видео показывают «не задано».
|
||||||
|
- Проверки: 118 тестов, type-check, lint, lint:css, сборка, Electron GUI smoke,
|
||||||
|
локальные Markdown-ссылки и git diff --check — PASS. Проверены отсутствие/null
|
||||||
|
scenario/story, неверные типы, приоритет ссылок состояния и очистка медиа,
|
||||||
|
восстановление кеша, подключение/локальное воспроизведение без истории и показ
|
||||||
|
текста/таймера/музыки без основного файла. GUI использует GetGame со story:null,
|
||||||
|
получает вступление из GetScreenState, затем проверяет явное отсутствие вступления.
|
||||||
|
Ожидание готовности списка игр в smoke уточнено после раннего нажатия выбора.
|
||||||
|
- Границы: изменения только evening_detective_screen. Сервер, proto и клиент
|
||||||
|
генерации не менялись. Production-подключение не проверялось; установочные
|
||||||
|
архивы не пересобирались, commit/push не выполнялись.
|
||||||
|
|||||||
+7
-1
@@ -247,5 +247,11 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
|
|||||||
Подробности ошибки видны прямо в пульте: операция, метод и путь API, статус HTTP
|
Подробности ошибки видны прямо в пульте: операция, метод и путь API, статус HTTP
|
||||||
или безопасный код сетевого сбоя. Если JSON имеет неверную структуру, сообщение
|
или безопасный код сетевого сбоя. Если JSON имеет неверную структуру, сообщение
|
||||||
показывает поле (включая индекс элемента), ожидаемый и фактический тип, например:
|
показывает поле (включая индекс элемента), ожидаемый и фактический тип, например:
|
||||||
`Описание игры · GET /api/games/7: Некорректный ответ API: поле game.scenario.story — ожидалось объект, получено null`.
|
`Описание игры · GET /api/games/7: Некорректный ответ API: поле game.scenario.story — ожидалось объект, получено массив`.
|
||||||
Содержимое ответа, значения полей, токены и stack trace не отображаются.
|
Содержимое ответа, значения полей, токены и stack trace не отображаются.
|
||||||
|
|
||||||
|
Отсутствие истории сценария в ответе игры не мешает подключению. После подключения
|
||||||
|
ссылки на обложку, фон, вступление и видео берутся из состояния экрана и автоматически
|
||||||
|
сохраняются локально. До получения состояния используются доступные ссылки игры.
|
||||||
|
Если медиа не заданы, текст, таймер и музыка остаются доступны; экран показывает
|
||||||
|
«Вступление не задано» или «Видео закрывашки не задано» для соответствующего шага.
|
||||||
|
|||||||
+83
-6
@@ -37,6 +37,8 @@ async function fixture(t: TestContext, secure = true) {
|
|||||||
offline: false,
|
offline: false,
|
||||||
server: 'https://server.test',
|
server: 'https://server.test',
|
||||||
introURL: 'https://media.test/intro.wav',
|
introURL: 'https://media.test/intro.wav',
|
||||||
|
omitScenario: false,
|
||||||
|
omitStory: false,
|
||||||
musicOverride: null as Record<string, unknown> | null,
|
musicOverride: null as Record<string, unknown> | null,
|
||||||
mediaHandler: null as ((url: string) => Promise<Response>) | null,
|
mediaHandler: null as ((url: string) => Promise<Response>) | null,
|
||||||
failMedia: false,
|
failMedia: false,
|
||||||
@@ -51,6 +53,7 @@ async function fixture(t: TestContext, secure = true) {
|
|||||||
roles: ['organizer'],
|
roles: ['organizer'],
|
||||||
remote: {
|
remote: {
|
||||||
gameID: 7,
|
gameID: 7,
|
||||||
|
introAudio: 'https://media.test/intro.wav',
|
||||||
mode: 2,
|
mode: 2,
|
||||||
command: 1,
|
command: 1,
|
||||||
stateVersion: '9007199254740993',
|
stateVersion: '9007199254740993',
|
||||||
@@ -139,12 +142,16 @@ async function fixture(t: TestContext, secure = true) {
|
|||||||
game: {
|
game: {
|
||||||
ID: 7,
|
ID: 7,
|
||||||
name: model.title,
|
name: model.title,
|
||||||
scenario: {
|
scenario: model.omitScenario
|
||||||
story: {
|
? null
|
||||||
introduction: { audio: model.introURL },
|
: {
|
||||||
conclusion: { video: '' },
|
story: model.omitStory
|
||||||
},
|
? null
|
||||||
},
|
: {
|
||||||
|
introduction: { audio: model.introURL },
|
||||||
|
conclusion: { video: '' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (path === '/api/games/7/screen-plan') {
|
if (path === '/api/games/7/screen-plan') {
|
||||||
@@ -1255,3 +1262,73 @@ test('некорректное серверное состояние отобр
|
|||||||
assert.ok(message.includes('получено object'))
|
assert.ok(message.includes('получено object'))
|
||||||
assert.ok(!message.includes('SECRET'))
|
assert.ok(!message.includes('SECRET'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const missing of ['scenario', 'story'] as const) {
|
||||||
|
test(`подключение без ${missing}: медиа состояния скачиваются и воспроизводятся локально`, async (t) => {
|
||||||
|
const f = await fixture(t)
|
||||||
|
f.model.omitScenario = missing === 'scenario'
|
||||||
|
f.model.omitStory = missing === 'story'
|
||||||
|
await f.login()
|
||||||
|
await f.online.act({ action: 'connect', gameId: 7 })
|
||||||
|
assert.equal(f.online.snapshot().screenId, '12')
|
||||||
|
assert.equal(f.controller.state.outputOpen, false)
|
||||||
|
f.controller.output(true)
|
||||||
|
await f.online.poll()
|
||||||
|
const state = f.controller.state
|
||||||
|
const pkg = f.store.description(state.catalog.key!)!
|
||||||
|
assert.ok(pkg.package.media.intro)
|
||||||
|
assert.equal(state.channels.main.desired, 'playing')
|
||||||
|
assert.equal(
|
||||||
|
state.channels.main.source?.url,
|
||||||
|
`screen-app://app/packages/${pkg.key}/${pkg.package.media.intro}`,
|
||||||
|
)
|
||||||
|
assert.ok((await f.store.localFile(pkg.key, pkg.package.media.intro!)).path)
|
||||||
|
const restored = new PackageStore(join(f.directory, 'packages'))
|
||||||
|
await restored.initialize()
|
||||||
|
assert.equal(restored.description(pkg.key)?.package.media.intro, pkg.package.media.intro)
|
||||||
|
assert.ok((await restored.localFile(pkg.key, pkg.package.media.intro!)).path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('без истории и медиа текст, таймер и музыка работают; отсутствие вступления не мешает связи', async (t) => {
|
||||||
|
const f = await fixture(t)
|
||||||
|
f.model.omitScenario = true
|
||||||
|
f.model.plan = parsePlan({
|
||||||
|
steps: [
|
||||||
|
{ mode: 8, text: 'Текст без истории' },
|
||||||
|
{ mode: 7, timerDurationSeconds: 30, timerLabel: 'Перерыв' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
f.model.remote = {
|
||||||
|
...f.model.remote,
|
||||||
|
mode: 8,
|
||||||
|
text: 'Текст без истории',
|
||||||
|
command: 0,
|
||||||
|
introAudio: '',
|
||||||
|
music: objectMusic(1),
|
||||||
|
}
|
||||||
|
await f.login()
|
||||||
|
await f.online.act({ action: 'connect', gameId: 7 })
|
||||||
|
f.controller.output(true)
|
||||||
|
await f.online.poll()
|
||||||
|
assert.equal(f.controller.state.scene.text, 'Текст без истории')
|
||||||
|
assert.equal(f.controller.state.channels.music.desired, 'playing')
|
||||||
|
f.model.remote = {
|
||||||
|
...f.model.remote,
|
||||||
|
mode: 7,
|
||||||
|
timerDurationSeconds: 30,
|
||||||
|
timerRemainingSeconds: 30,
|
||||||
|
timerState: 1,
|
||||||
|
stateVersion: '9007199254740994',
|
||||||
|
}
|
||||||
|
await f.online.poll()
|
||||||
|
assert.equal(f.controller.state.scene.mode, 7)
|
||||||
|
assert.equal(f.controller.state.timer.durationMs, 30000)
|
||||||
|
f.model.remote = { ...f.model.remote, mode: 2, stateVersion: '9007199254740995' }
|
||||||
|
await f.online.poll()
|
||||||
|
assert.equal(f.controller.state.scene.mode, 2)
|
||||||
|
assert.equal(f.controller.state.channels.main.source, null)
|
||||||
|
assert.equal(f.controller.state.channels.music.desired, 'playing')
|
||||||
|
assert.equal(f.online.snapshot().connection, 'connected')
|
||||||
|
assert.equal(f.online.snapshot().preparation.error, '')
|
||||||
|
})
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export class Online {
|
|||||||
{
|
{
|
||||||
server: this.credentials.server,
|
server: this.credentials.server,
|
||||||
gameId,
|
gameId,
|
||||||
|
resources: this.received?.gameId === gameId ? this.received.resources : undefined,
|
||||||
token: this.credentials.access,
|
token: this.credentials.access,
|
||||||
signal: this.materialAbort.signal,
|
signal: this.materialAbort.signal,
|
||||||
onRequestFailure: this.onRequestFailure,
|
onRequestFailure: this.onRequestFailure,
|
||||||
@@ -1171,6 +1172,7 @@ export class Online {
|
|||||||
{
|
{
|
||||||
server: this.state.server,
|
server: this.state.server,
|
||||||
gameId,
|
gameId,
|
||||||
|
resources: this.received?.gameId === gameId ? this.received.resources : undefined,
|
||||||
token: this.credentials.access,
|
token: this.credentials.access,
|
||||||
signal: this.cancellation.signal,
|
signal: this.cancellation.signal,
|
||||||
onRequestFailure: this.onRequestFailure,
|
onRequestFailure: this.onRequestFailure,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { test } from 'node:test'
|
import { test } from 'node:test'
|
||||||
import { ApiError, SessionChangedError } from './request-error.js'
|
import { ApiError, SessionChangedError } from './request-error.js'
|
||||||
import { buildScreenPackage, prepareScreenPackage } from './screen-package.js'
|
import { buildScreenPackage, prepareScreenPackage, parseScreenPackage } from './screen-package.js'
|
||||||
|
|
||||||
const server = 'https://example.test'
|
const server = 'https://example.test'
|
||||||
function fixture() {
|
function fixture() {
|
||||||
@@ -151,7 +151,7 @@ test('неполные или некорректные ответы не ста
|
|||||||
assert.throws(() => build(data))
|
assert.throws(() => build(data))
|
||||||
}
|
}
|
||||||
assert.throws(() => buildScreenPackage(server, 7, { game: {}, plan: {}, music: {} }))
|
assert.throws(() => buildScreenPackage(server, 7, { game: {}, plan: {}, music: {} }))
|
||||||
assert.throws(() =>
|
assert.doesNotThrow(() =>
|
||||||
buildScreenPackage(server, 7, { game: { game: { ID: 7, scenario: {} } }, plan: {}, music: {} }),
|
buildScreenPackage(server, 7, { game: { game: { ID: 7, scenario: {} } }, plan: {}, music: {} }),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -306,8 +306,6 @@ test('таймаут и отмена подготовки различаются
|
|||||||
|
|
||||||
test('ошибка структуры описания называет запрос, поле и тип, не раскрывая значения', async () => {
|
test('ошибка структуры описания называет запрос, поле и тип, не раскрывая значения', async () => {
|
||||||
for (const [badValue, expectedType] of [
|
for (const [badValue, expectedType] of [
|
||||||
[null, 'null'],
|
|
||||||
[undefined, 'поле отсутствует'],
|
|
||||||
[[], 'массив'],
|
[[], 'массив'],
|
||||||
['SECRET', 'string'],
|
['SECRET', 'string'],
|
||||||
] as const) {
|
] as const) {
|
||||||
@@ -366,3 +364,62 @@ test('ошибки элементов плана и треков сохраня
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('scenario и story могут отсутствовать или быть null; неверные типы остаются ошибкой', () => {
|
||||||
|
for (const scenario of [undefined, null, {}, { story: null }, { story: undefined }]) {
|
||||||
|
const pkg = buildScreenPackage(server, 7, {
|
||||||
|
game: { game: { ID: 7, scenario } },
|
||||||
|
plan: {
|
||||||
|
plan: {
|
||||||
|
steps: [
|
||||||
|
{ mode: 8, text: 'Текст' },
|
||||||
|
{ mode: 7, timerDurationSeconds: 60 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
music: {},
|
||||||
|
})
|
||||||
|
assert.deepEqual(pkg.assets, [])
|
||||||
|
assert.deepEqual(pkg.media, { cover: null, background: null, intro: null, conclusion: null })
|
||||||
|
assert.equal(pkg.steps.length, 2)
|
||||||
|
}
|
||||||
|
for (const scenario of [[], 'SECRET', { story: [] }, { story: 'SECRET' }]) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
buildScreenPackage(server, 7, { game: { game: { ID: 7, scenario } }, plan: {}, music: {} }),
|
||||||
|
/game.scenario/,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('ссылки состояния экрана определяют медиа без истории, включая явное отсутствие файла', () => {
|
||||||
|
const resources = {
|
||||||
|
cover: 'cover.png',
|
||||||
|
background: 'background.png',
|
||||||
|
intro: 'intro.wav',
|
||||||
|
conclusion: 'end.mp4',
|
||||||
|
}
|
||||||
|
const minimal = { game: { game: { ID: 7 } }, plan: {}, music: {} }
|
||||||
|
const pkg = buildScreenPackage(server, 7, minimal, resources)
|
||||||
|
assert.equal(pkg.assets.length, 4)
|
||||||
|
for (const [name, ref] of Object.entries(resources)) {
|
||||||
|
const id = pkg.media[name as keyof typeof pkg.media]
|
||||||
|
assert.equal(pkg.assets.find((a) => a.id === id)?.url, `${server}/api/files/${ref}`)
|
||||||
|
}
|
||||||
|
const cleared = buildScreenPackage(server, 7, fixture(), {
|
||||||
|
cover: '',
|
||||||
|
background: '',
|
||||||
|
intro: '',
|
||||||
|
conclusion: '',
|
||||||
|
})
|
||||||
|
assert.deepEqual(cleared.media, { cover: null, background: null, intro: null, conclusion: null })
|
||||||
|
assert.ok(!cleared.assets.some((a) => a.url.endsWith('final.mp4')))
|
||||||
|
const coverOnly = buildScreenPackage(server, 7, minimal, {
|
||||||
|
cover: 'cover.png',
|
||||||
|
background: '',
|
||||||
|
intro: '',
|
||||||
|
conclusion: '',
|
||||||
|
})
|
||||||
|
assert.equal(coverOnly.media.background, null)
|
||||||
|
for (const value of [pkg, cleared, coverOnly]) assert.deepEqual(parseScreenPackage(value), value)
|
||||||
|
})
|
||||||
|
|||||||
+64
-37
@@ -37,6 +37,7 @@ export interface PackageRequest {
|
|||||||
server: string
|
server: string
|
||||||
gameId: number
|
gameId: number
|
||||||
token: string
|
token: string
|
||||||
|
resources?: { cover: string; background: string; intro: string; conclusion: string }
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
onRequestFailure?: (failure: {
|
onRequestFailure?: (failure: {
|
||||||
path: string
|
path: string
|
||||||
@@ -149,6 +150,7 @@ export function buildScreenPackage(
|
|||||||
server: string,
|
server: string,
|
||||||
gameId: number,
|
gameId: number,
|
||||||
responses: { game: unknown; plan: unknown; music: unknown },
|
responses: { game: unknown; plan: unknown; music: unknown },
|
||||||
|
resources?: PackageRequest['resources'],
|
||||||
): ScreenPackage {
|
): ScreenPackage {
|
||||||
server = serverURL(server)
|
server = serverURL(server)
|
||||||
id(gameId)
|
id(gameId)
|
||||||
@@ -160,8 +162,8 @@ export function buildScreenPackage(
|
|||||||
game.ID,
|
game.ID,
|
||||||
'API вернул другую игру',
|
'API вернул другую игру',
|
||||||
)
|
)
|
||||||
const scenario = object(game.scenario, 'game.scenario')
|
const scenario = optionalObject(game.scenario, 'game.scenario')
|
||||||
const story = object(scenario.story, 'game.scenario.story')
|
const story = optionalObject(scenario.story, 'game.scenario.story')
|
||||||
const intro = optionalObject(story.introduction, 'game.scenario.story.introduction')
|
const intro = optionalObject(story.introduction, 'game.scenario.story.introduction')
|
||||||
const conclusion = optionalObject(story.conclusion, 'game.scenario.story.conclusion')
|
const conclusion = optionalObject(story.conclusion, 'game.scenario.story.conclusion')
|
||||||
const assets = new Map<string, { id: string; url: string }>()
|
const assets = new Map<string, { id: string; url: string }>()
|
||||||
@@ -183,12 +185,20 @@ export function buildScreenPackage(
|
|||||||
assets.set(assetId, { id: assetId, url })
|
assets.set(assetId, { id: assetId, url })
|
||||||
return assetId
|
return assetId
|
||||||
}
|
}
|
||||||
const cover = asset(scenario.image, 'game.scenario.image')
|
const cover = resources
|
||||||
|
? asset(resources.cover, 'state.coverImage')
|
||||||
|
: asset(scenario.image, 'game.scenario.image')
|
||||||
const media = {
|
const media = {
|
||||||
cover,
|
cover,
|
||||||
background: asset(story.background, 'game.scenario.story.background') ?? cover,
|
background: resources
|
||||||
intro: asset(intro.audio, 'game.scenario.story.introduction.audio'),
|
? asset(resources.background, 'state.backgroundImage')
|
||||||
conclusion: asset(conclusion.video, 'game.scenario.story.conclusion.video'),
|
: (asset(story.background, 'game.scenario.story.background') ?? cover),
|
||||||
|
intro: resources
|
||||||
|
? asset(resources.intro, 'state.introAudio')
|
||||||
|
: asset(intro.audio, 'game.scenario.story.introduction.audio'),
|
||||||
|
conclusion: resources
|
||||||
|
? asset(resources.conclusion, 'state.conclusionVideo')
|
||||||
|
: asset(conclusion.video, 'game.scenario.story.conclusion.video'),
|
||||||
}
|
}
|
||||||
const plan = optionalObject(object(responses.plan, 'planResponse').plan, 'plan')
|
const plan = optionalObject(object(responses.plan, 'planResponse').plan, 'plan')
|
||||||
const steps = list(plan.steps, 'plan.steps').map((value, index) => {
|
const steps = list(plan.steps, 'plan.steps').map((value, index) => {
|
||||||
@@ -374,9 +384,10 @@ export async function prepareScreenPackage(
|
|||||||
client.GetGameMusic({ gameID: request.gameId }),
|
client.GetGameMusic({ gameID: request.gameId }),
|
||||||
])
|
])
|
||||||
try {
|
try {
|
||||||
return buildScreenPackage(server, request.gameId, { game, plan, music })
|
return buildScreenPackage(server, request.gameId, { game, plan, music }, request.resources)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const field = error instanceof ResponseFieldError ? error.field : 'game'
|
const field = error instanceof ResponseFieldError ? error.field : 'game'
|
||||||
|
const state = field.startsWith('state.')
|
||||||
const suffix = field.startsWith('plan')
|
const suffix = field.startsWith('plan')
|
||||||
? '/screen-plan'
|
? '/screen-plan'
|
||||||
: field.startsWith('music')
|
: field.startsWith('music')
|
||||||
@@ -384,9 +395,15 @@ export async function prepareScreenPackage(
|
|||||||
: ''
|
: ''
|
||||||
throw requestErrorContext(
|
throw requestErrorContext(
|
||||||
error,
|
error,
|
||||||
suffix === '/music' ? 'GetGameMusic' : suffix ? 'GetScreenPlan' : 'GetGame',
|
state
|
||||||
'GET',
|
? 'GetScreenState'
|
||||||
`/api/games/${request.gameId}${suffix}`,
|
: suffix === '/music'
|
||||||
|
? 'GetGameMusic'
|
||||||
|
: suffix
|
||||||
|
? 'GetScreenPlan'
|
||||||
|
: 'GetGame',
|
||||||
|
state ? 'POST' : 'GET',
|
||||||
|
state ? '/api/screens/state' : `/api/games/${request.gameId}${suffix}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,41 +448,51 @@ export function parseScreenPackage(value: unknown): ScreenPackage {
|
|||||||
const selectedId = integer(music.selectedPlaylistId)
|
const selectedId = integer(music.selectedPlaylistId)
|
||||||
const selected = selectedId ? playlists.find((playlist) => playlist.ID === selectedId) : undefined
|
const selected = selectedId ? playlists.find((playlist) => playlist.ID === selectedId) : undefined
|
||||||
if (selectedId && !selected) throw new Error('Отсутствует выбранный плейлист')
|
if (selectedId && !selected) throw new Error('Отсутствует выбранный плейлист')
|
||||||
const result = buildScreenPackage(text(data.server), id(game.id), {
|
const result = buildScreenPackage(
|
||||||
game: {
|
text(data.server),
|
||||||
|
id(game.id),
|
||||||
|
{
|
||||||
game: {
|
game: {
|
||||||
ID: game.id,
|
game: {
|
||||||
name: text(game.name),
|
ID: game.id,
|
||||||
scenario: {
|
name: text(game.name),
|
||||||
image: ref(media.cover),
|
scenario: {
|
||||||
story: {
|
image: ref(media.cover),
|
||||||
background: ref(media.background),
|
story: {
|
||||||
introduction: { audio: ref(media.intro) },
|
background: ref(media.background),
|
||||||
conclusion: { video: ref(media.conclusion) },
|
introduction: { audio: ref(media.intro) },
|
||||||
|
conclusion: { video: ref(media.conclusion) },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
plan: {
|
|
||||||
plan: {
|
plan: {
|
||||||
steps: list(data.steps).map((value) => {
|
plan: {
|
||||||
const step = object(value)
|
steps: list(data.steps).map((value) => {
|
||||||
return {
|
const step = object(value)
|
||||||
mode: step.mode,
|
return {
|
||||||
text: step.text,
|
mode: step.mode,
|
||||||
timerLabel: step.timerLabel,
|
text: step.text,
|
||||||
timerDurationSeconds: step.timerDurationSeconds,
|
timerLabel: step.timerLabel,
|
||||||
}
|
timerDurationSeconds: step.timerDurationSeconds,
|
||||||
}),
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
music: {
|
||||||
|
scenarioPlaylistId: integer(music.scenarioPlaylistId),
|
||||||
|
gamePlaylistId: integer(music.gamePlaylistId),
|
||||||
|
state: { playlist: selected },
|
||||||
|
playlists,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
music: {
|
{
|
||||||
scenarioPlaylistId: integer(music.scenarioPlaylistId),
|
cover: ref(media.cover),
|
||||||
gamePlaylistId: integer(music.gamePlaylistId),
|
background: ref(media.background),
|
||||||
state: { playlist: selected },
|
intro: ref(media.intro),
|
||||||
playlists,
|
conclusion: ref(media.conclusion),
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
if (result.metadataVersion !== data.metadataVersion || result.assets.length !== assets.size)
|
if (result.metadataVersion !== data.metadataVersion || result.assets.length !== assets.size)
|
||||||
throw new Error('Повреждены метаданные пакета')
|
throw new Error('Повреждены метаданные пакета')
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ module.exports = async function onlineSmoke({
|
|||||||
let gamesCalls = 0
|
let gamesCalls = 0
|
||||||
let varyGames = false
|
let varyGames = false
|
||||||
let invalidStory = false
|
let invalidStory = false
|
||||||
|
let introAudio = 'intro.wav'
|
||||||
let version = 1
|
let version = 1
|
||||||
let offline = false
|
let offline = false
|
||||||
let polls = 0
|
let polls = 0
|
||||||
@@ -64,7 +65,7 @@ module.exports = async function onlineSmoke({
|
|||||||
game: {
|
game: {
|
||||||
ID: 6,
|
ID: 6,
|
||||||
name: 'HTTP smoke',
|
name: 'HTTP smoke',
|
||||||
scenario: { story: invalidStory ? null : { introduction: { audio: 'intro.wav' } } },
|
scenario: { story: invalidStory ? [] : null },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (path === '/api/games/6/screen-plan') {
|
if (path === '/api/games/6/screen-plan') {
|
||||||
@@ -127,7 +128,7 @@ module.exports = async function onlineSmoke({
|
|||||||
state: {
|
state: {
|
||||||
gameID: 6,
|
gameID: 6,
|
||||||
music,
|
music,
|
||||||
introAudio: 'intro.wav',
|
introAudio,
|
||||||
mode: remoteMode,
|
mode: remoteMode,
|
||||||
text: remoteText,
|
text: remoteText,
|
||||||
timerLabel,
|
timerLabel,
|
||||||
@@ -229,6 +230,14 @@ module.exports = async function onlineSmoke({
|
|||||||
)
|
)
|
||||||
varyGames = false
|
varyGames = false
|
||||||
await act({ action: 'games' })
|
await act({ action: 'games' })
|
||||||
|
await poll(
|
||||||
|
() =>
|
||||||
|
evaluate(`(() => {
|
||||||
|
const select = Array.from(document.querySelectorAll('.n-base-selection')).find(e => e.textContent.includes('Выберите игру для подготовки'))
|
||||||
|
return !!select && !select.classList.contains('n-base-selection--disabled') && !document.body.textContent.includes('Список игр пока пуст')
|
||||||
|
})()`),
|
||||||
|
'updated game list rendered',
|
||||||
|
)
|
||||||
await evaluate(
|
await evaluate(
|
||||||
`Array.from(document.querySelectorAll('.n-base-selection')).find(e => e.textContent.includes('Выберите игру для подготовки')).click()`,
|
`Array.from(document.querySelectorAll('.n-base-selection')).find(e => e.textContent.includes('Выберите игру для подготовки')).click()`,
|
||||||
)
|
)
|
||||||
@@ -264,7 +273,7 @@ module.exports = async function onlineSmoke({
|
|||||||
`document.querySelector('[data-testid="connection-error"]').textContent`,
|
`document.querySelector('[data-testid="connection-error"]').textContent`,
|
||||||
)
|
)
|
||||||
assert.ok(errorText.includes('Описание игры · GET /api/games/6'))
|
assert.ok(errorText.includes('Описание игры · GET /api/games/6'))
|
||||||
assert.ok(errorText.includes('получено null'))
|
assert.ok(errorText.includes('получено массив'))
|
||||||
assert.ok(!errorText.includes('TEST-ACCESS'))
|
assert.ok(!errorText.includes('TEST-ACCESS'))
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await evaluate(
|
await evaluate(
|
||||||
@@ -418,6 +427,19 @@ module.exports = async function onlineSmoke({
|
|||||||
)
|
)
|
||||||
assert.match((await state()).channels.music.source.url, /^screen-app:/)
|
assert.match((await state()).channels.music.source.url, /^screen-app:/)
|
||||||
assert.notEqual((await state()).channels.music.source.url, oldMusic)
|
assert.notEqual((await state()).channels.music.source.url, oldMusic)
|
||||||
|
introAudio = ''
|
||||||
|
remoteMode = 2
|
||||||
|
currentCommand = 0
|
||||||
|
version++
|
||||||
|
await poll(
|
||||||
|
async () => (await state()).scene.mode === 2 && !(await state()).channels.main.source,
|
||||||
|
'screen without intro accepted',
|
||||||
|
)
|
||||||
|
await poll(
|
||||||
|
() => inspect(`document.body.textContent.includes('Вступление не задано')`),
|
||||||
|
'missing intro message',
|
||||||
|
)
|
||||||
|
assert.equal((await state()).channels.music.desired, 'playing')
|
||||||
console.log('SCREEN_LIVE_MATERIALS_SMOKE_PASS')
|
console.log('SCREEN_LIVE_MATERIALS_SMOKE_PASS')
|
||||||
// Настоящий редактор: черновик, ошибка сохранения, порядок и переходы.
|
// Настоящий редактор: черновик, ошибка сохранения, порядок и переходы.
|
||||||
const clickPlan = async (label) => {
|
const clickPlan = async (label) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user