import { test } from 'node:test' import assert from 'node:assert/strict' import { nextTick, ref, type Ref } from 'vue' import { parseSettings } from '../guards' import { useScoreboard } from '../useScoreboard' import { DEFAULT_ANNOUNCE_SECONDS, createAnnouncement, resolveAnnounceText, type AnnouncementController, } from '../announce' import type { DisplayPageKey } from '../types' // 公告分两段:全屏 seconds 秒 + 底栏常驻。计时结束只退全屏,正文必须留着, // 只有新公告或 announce_clear 才换掉它——曾经这里跟着计时器一起清空, // 底栏「公告」槽位立刻回落成「暂无公告」。 interface Harness { page: Ref events: string[] controller: AnnouncementController /** 当前公告正文;读成函数,避免 assert.equal 的类型收窄把后续读取判成 never。 */ text: () => string | null } /** 复刻 useScoreboard 里的挂接方式:进全屏切到公告页,退全屏切回返回页。 */ function setup(initialPage: DisplayPageKey = 'standings'): Harness { const page = ref(initialPage) const events: string[] = [] const controller = createAnnouncement({ page, onFullscreenEnter: () => { events.push('enter') page.value = 'announcement' }, onFullscreenExit: (returnPage) => { events.push(`exit:${returnPage}`) if (page.value === 'announcement') page.value = returnPage }, }) return { page, events, controller, text: () => controller.current.value?.text ?? null } } test('推送公告:到点只退全屏,正文留在底栏', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('standings') host.controller.push('请各队到检录处集合', 10) assert.equal(host.text(), '请各队到检录处集合') assert.equal(host.controller.current.value?.seconds, 10) assert.equal(host.page.value, 'announcement', '推送即进全屏') t.mock.timers.tick(9_000) assert.equal(host.page.value, 'announcement', '没到点不能提前退场') t.mock.timers.tick(1_000) assert.equal(host.page.value, 'standings', '到点回到进入公告前的页') assert.deepEqual(host.events, ['enter', 'exit:standings']) assert.equal( host.text(), '请各队到检录处集合', '正文不能跟着计时器一起清掉,底栏要继续显示', ) }) test('清除公告:退全屏并清空正文,计时器不再触发', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('team-medals') host.controller.push('临时通知', 30) host.controller.clear() assert.equal(host.controller.current.value, null) assert.equal(host.page.value, 'team-medals', '全屏中清除要回到进入公告前的页') t.mock.timers.tick(60_000) assert.deepEqual(host.events, ['enter', 'exit:team-medals'], '清除后不能再退场一次') }) test('全屏已结束时清除:只清正文,不动当前页', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('standings') host.controller.push('已过期的公告', 5) t.mock.timers.tick(5_000) host.controller.clear() assert.equal(host.controller.current.value, null) assert.equal(host.page.value, 'standings') assert.deepEqual(host.events, ['enter', 'exit:standings']) }) test('没推过公告时清除是空操作:手动停在公告页不会被踢走', () => { const host = setup('announcement') host.controller.clear() assert.equal(host.controller.current.value, null) assert.equal(host.page.value, 'announcement') assert.deepEqual(host.events, []) }) test('空白公告被忽略,不进全屏也不顶掉当前公告', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('standings') host.controller.push(' ', 10) host.controller.push('', 10) assert.equal(host.controller.current.value, null) assert.equal(host.page.value, 'standings') assert.deepEqual(host.events, []) host.controller.push('正式公告', 10) host.controller.push(' ', 10) assert.equal(host.text(), '正式公告') }) test('连推两条:返回页始终是进入公告前那一页', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('event-results') host.controller.push('第一条', 10) t.mock.timers.tick(3_000) // 第二条到来时 page 已经是 announcement,不能把返回页记成公告页自身。 host.controller.push('第二条', 10) assert.equal(host.text(), '第二条') t.mock.timers.tick(10_000) assert.equal(host.page.value, 'event-results') // 第二条会再进一次全屏(页已在公告页,进入动作是幂等的),但返回页仍是 event-results。 assert.deepEqual(host.events, ['enter', 'enter', 'exit:event-results']) assert.equal(host.text(), '第二条', '换成第二条后仍常驻底栏') }) test('seconds=0 表示不定时全屏,只能由清除收场', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('standings') host.controller.push('长期公告', 0) t.mock.timers.tick(600_000) assert.equal(host.page.value, 'announcement') host.controller.clear() assert.equal(host.page.value, 'standings') assert.equal(host.controller.current.value, null) }) test('dispose 停表:卸载后不再自动退场', (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const host = setup('standings') host.controller.push('公告', 10) host.controller.dispose() t.mock.timers.tick(10_000) assert.deepEqual(host.events, ['enter']) assert.equal(host.text(), '公告') }) test('公告取值优先级:推送中的实时值 > 服务端持久化 > 占位', () => { const live = { text: '实时公告', seconds: 10, startedAt: 1 } assert.equal(resolveAnnounceText(live, '持久化公告'), '实时公告') assert.equal(resolveAnnounceText(null, '持久化公告'), '持久化公告', '大屏错过指令时靠设置兜底') assert.equal(resolveAnnounceText(null, null), '暂无公告') assert.equal(resolveAnnounceText(null, undefined), '暂无公告') assert.equal(resolveAnnounceText(null, ' '), '暂无公告') assert.equal(resolveAnnounceText({ ...live, text: ' ' }, '持久化公告'), '持久化公告') assert.equal(resolveAnnounceText(null, ' 两侧留白 '), '两侧留白') }) test('parseSettings 归一公告:嵌套与扁平键都认,空白当没有', () => { assert.equal(parseSettings({}).display.announce, null) assert.equal(parseSettings({ display: { announce: '注意安全' } }).display.announce, '注意安全') assert.equal(parseSettings({ 'display.announce_text': '注意安全' }).display.announce, '注意安全') assert.equal(parseSettings({ display: { announce: ' ' } }).display.announce, null) assert.equal(parseSettings({ display: { announce: 42 } }).display.announce, null) }) // 大屏可能整条指令都没收到(断线、后开窗口),所以设置里的公告也要驱动同一套状态机。 test('快照里的公告变化:新公告进全屏,同一条不重复,服务端清空后跟着清', async (t) => { t.mock.timers.enable({ apis: ['setTimeout'] }) const board = useScoreboard() board.settings.value = parseSettings({ display: { announce: '紧急通知' } }) await nextTick() assert.equal(board.announceText.value, '紧急通知') assert.equal(board.page.value, 'announcement', '设置里出现本机没有的公告时要广播全屏') t.mock.timers.tick(DEFAULT_ANNOUNCE_SECONDS * 1000) assert.equal(board.page.value, 'standings', '全屏到点退回原页') assert.equal(board.announceText.value, '紧急通知', '全屏结束后底栏仍留着这条公告') // 指令推过同一条后,持久化会跟着回推一次设置:不能再全屏一次。 board.settings.value = parseSettings({ display: { announce: '紧急通知' } }) await nextTick() assert.equal(board.page.value, 'standings') assert.equal(board.announcement.value?.text, '紧急通知') board.settings.value = parseSettings({}) await nextTick() assert.equal(board.announcement.value, null) assert.equal(board.announceText.value, '暂无公告') })