import assert from 'node:assert/strict' import { test } from 'node:test' import { Controller } from './controller.js' import { parsePlan } from './screen-plan.js' import { parseRemote, applyRemote } from './remote-state.js' import { parseTeams } from './teams.js' import { buildScreenPackage, parseScreenPackage } from './screen-package.js' import { changeTimerType, validQR } from '../src/utils/presentation.js' test('QR validation and timer captions preserve custom text', () => { for (const url of ['', 'javascript:alert(1)', 'https:///missing', 'https://u:p@example.org']) assert.equal(validQR(url), false) assert.equal(validQR('https://example.org/?q=1#join'), true) assert.equal(validQR('https://example.org', 'я'.repeat(5001)), false) assert.equal(changeTimerType('Перерыв', 0, 1), 'До начала игры') assert.equal(changeTimerType('До начала игры', 1, 2), 'До конца игры') assert.equal(changeTimerType('Обсуждение', 0, 2), 'Обсуждение') assert.equal(changeTimerType('', 2, 0), 'Перерыв') const c = new Controller(() => {}) c.command({ session: c.state.session, sequence: 1, action: 'timer-set', timerType: 1, label: '', durationSeconds: 60 }) assert.equal(c.state.timer.label, 'До начала игры') }) test('new screens survive plan/package round trip and local playback', () => { const plan = parsePlan({ steps: [ { mode: 'SCREEN_MODE_QR', qrUrl: 'https://example.org/join', text: 'Вступайте' }, { mode: 'SCREEN_MODE_TEAMS' }, { mode: 7, timerType: 'SCREEN_TIMER_TYPE_UNTIL_END', timerDurationSeconds: 60 }, ], }) assert.equal(plan.steps[2]!.timerLabel, 'До конца игры') const pkg = buildScreenPackage('https://example.org', 1, { game: { game: { ID: 1, name: 'Игра' } }, plan: { plan }, music: {}, }) assert.deepEqual(parseScreenPackage(pkg).steps, pkg.steps) const c = new Controller(() => {}) c.setPlan(plan.steps) const command = (index: number) => c.command({ session: c.state.session, sequence: c.state.sequence + 1, action: 'step', index }) command(0) assert.equal(c.state.scene.qrUrl, 'https://example.org/join') assert.equal(c.state.scene.text, 'Вступайте') command(1) assert.equal(c.state.scene.mode, 10) command(2) assert.equal(c.state.timer.timerType, 2) assert.equal(c.state.timer.status, 'stopped') assert.throws(() => parsePlan({ steps: [{ mode: 9, qrUrl: 'javascript:alert(1)' }] })) assert.throws(() => parsePlan({ steps: [{ mode: 7, timerDurationSeconds: 60, timerType: 3 }] })) const legacy = parsePlan({ steps: [{ mode: 7, timerLabel: 'Мой текст', timerDurationSeconds: 60 }], }) assert.equal(legacy.steps[0]!.timerType ?? 0, 0) assert.equal(legacy.steps[0]!.timerLabel, 'Мой текст') }) test('remote QR changes and timer types reach the output controller', () => { const c = new Controller(() => {}) const previous = parseRemote({ mode: 1, stateVersion: '1' }) const qr = parseRemote({ mode: 9, stateVersion: '2', qrUrl: 'https://example.org', text: 'Текст', }) applyRemote(c, previous, qr) assert.equal(c.state.scene.qrUrl, qr.qrUrl) const timer = parseRemote({ mode: 7, stateVersion: '3', timerType: 'SCREEN_TIMER_TYPE_BEFORE_GAME', }) applyRemote(c, qr, timer) assert.equal(c.state.timer.timerType, 1) }) test('team snapshots sort names, strip private fields and reject another game', () => { const packageKey = 'a'.repeat(64) const snapshot = { packageKey, capturedAt: '2026-10-23T12:00:00Z', teams: [ { id: '2', name: 'Ястребы', password: 'SECRET' }, { id: '1', name: 'Агенты' }, ], } const parsed = parseTeams(snapshot, packageKey) assert.deepEqual( parsed.teams.map((team) => team.name), ['Агенты', 'Ястребы'], ) assert.equal(JSON.stringify(parsed).includes('SECRET'), false) assert.throws(() => parseTeams(snapshot, 'b'.repeat(64))) assert.throws(() => parseTeams({ ...snapshot, teams: [snapshot.teams[0], snapshot.teams[0]] }, packageKey), ) assert.equal(parseTeams({ ...snapshot, teams: [] }, packageKey).teams.length, 0) })