import { test } from 'node:test' import assert from 'node:assert/strict' /** * 大屏控制的接口契约:在线实例列表的字段、定向 goto 的请求形状,以及 404 原文的传递。 * * 现场靠这份列表判断「局域网设备到底连上没有」,字段一错就会把在线设备显示成未知; * 定向切页点名的实例全离线时服务端回 404,那句「目标大屏已离线」必须原样让操作员看到。 */ interface Captured { url?: string method?: string body?: unknown } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }) } let reply: () => Response = () => jsonResponse({ ok: true, data: null }) let lastRequest: Captured = {} // 必须在导入被测模块之前装好:ApiClient 在构造时就把当时的 fetch 绑进实例, // 之后再替换 globalThis.fetch 对它无效。所以这里装一个固定函数,每个用例只换回复。 globalThis.fetch = (async (input: unknown, init?: RequestInit) => { lastRequest = { url: String(input), method: init?.method ?? 'GET', body: init?.body } return reply() }) as typeof fetch function respondWith(response: () => Response): void { lastRequest = {} reply = response } const { ApiError, adminApi } = await import('../api/client') const CONSOLE_WINDOW = { id: 'LAPTOP-1-2528732444', online: true, page: 'standings', lastHeartbeat: '2026-09-11T10:00:00Z', connectedAt: '2026-09-11T09:00:00Z', remoteAddr: '127.0.0.1:52341', ackVersion: 42, slow: false, screen: '2', } /** 局域网浏览器:没屏号,且 page / remoteAddr 因服务端 omitempty 可能整个字段都不在。 */ const LAN_BROWSER = { id: 'display-9f3c11ab', online: true, lastHeartbeat: '2026-09-11T10:00:02Z', connectedAt: '2026-09-11T09:58:00Z', ackVersion: 7, slow: true, screen: '', } test('实例列表解析出屏号、来源 IP 与当前页', async () => { respondWith(() => jsonResponse({ ok: true, data: [CONSOLE_WINDOW, LAN_BROWSER] })) const list = await adminApi.displayInstances() assert.equal(lastRequest.url, '/api/display/instances') assert.equal(lastRequest.method, 'GET') assert.equal(list.length, 2) assert.deepEqual(list[0], CONSOLE_WINDOW) // 局域网浏览器没有屏号,缺字段的 page / remoteAddr 归一成空串而不是 undefined。 assert.equal(list[1]?.screen, '') assert.equal(list[1]?.remoteAddr, '') assert.equal(list[1]?.page, '') assert.equal(list[1]?.slow, true) }) test('实例列表不是数组时报解析失败,不当成空列表', async () => { respondWith(() => jsonResponse({ ok: true, data: { instances: [] } })) await assert.rejects(() => adminApi.displayInstances(), (error: Error) => error.message.includes('大屏实例列表'), ) }) test('定向 goto 带 targets,广播指令不带', async () => { respondWith(() => jsonResponse({ ok: true, data: null })) await adminApi.displayCommand({ action: 'goto', payload: { page: 'standings' }, targets: ['display-1'] }) assert.equal(lastRequest.url, '/api/display/command') assert.equal(lastRequest.method, 'POST') const targeted = JSON.parse(String(lastRequest.body)) as { action: string payload: { page: string } targets?: string[] } assert.equal(targeted.action, 'goto') assert.equal(targeted.payload.page, 'standings') assert.deepEqual(targeted.targets, ['display-1']) // 公告那类全屏指令必须继续走广播:带上 targets 就会只发给一台。 await adminApi.displayCommand({ action: 'announce', payload: { text: '集合', seconds: 10 } }) const broadcast = JSON.parse(String(lastRequest.body)) as Record assert.equal('targets' in broadcast, false) }) // 点名的实例全离线时服务端回 404;吞成「操作失败」现场就不知道该刷新列表了。 test('目标大屏离线时 404 原文原样抛出', async () => { respondWith(() => jsonResponse({ ok: false, error: { code: 'VALIDATION_FAILED', message: '目标大屏已离线,请刷新在线列表' } }, 404), ) await assert.rejects( () => adminApi.displayCommand({ action: 'goto', payload: { page: 'team-medals' }, targets: ['gone'] }), (error: unknown) => { assert.ok(error instanceof ApiError) assert.equal(error.status, 404) assert.equal(error.message, '目标大屏已离线,请刷新在线列表') return true }, ) }) // 看板每 5 秒轮询一次,后端没起来时给空列表就够了——这里弹错误只会盖住真正有用的信息。 test('后端连不上时实例列表给空数组,不抛错', async () => { respondWith(() => { throw new TypeError('fetch failed') }) assert.deepEqual(await adminApi.displayInstances(), []) }) // 「实时赛况」当前项目由服务端判定,前端只消费结论:source / eventIds / events 原样透传。 test('实时赛况接口透传判定结果与入选原因', async () => { respondWith(() => jsonResponse({ ok: true, data: { source: 'auto', eventIds: [7], events: [ { event: { id: 7, name: '100米跑' }, reason: 'recent', groups: [{ eventId: 7, categoryId: 1, categoryName: '男子', rows: [] }], }, ], }, }), ) const live = await adminApi.displayLive() assert.equal(lastRequest.url, '/api/display/live') assert.equal(lastRequest.method, 'GET') assert.equal(live.source, 'auto') assert.deepEqual(live.eventIds, [7]) assert.equal(live.events[0]?.event.name, '100米跑') assert.equal(live.events[0]?.reason, 'recent') assert.equal(live.events[0]?.groups.length, 1) }) // 判定依赖排期与最近发布,示例数据里没有这套时序,连不上时给空结论而不是抛错。 test('后端连不上时实时赛况给空结论', async () => { respondWith(() => { throw new TypeError('fetch failed') }) assert.deepEqual(await adminApi.displayLive(), { source: 'auto', eventIds: [], events: [] }) })