import { test } from 'node:test' import assert from 'node:assert/strict' import { RealtimeClient } from '@/shared' import { deviceLabel, type DeviceLabelSource } from '../deviceLabel' import { DISPLAY_INSTANCE_STORAGE_KEY, resolveDisplayIdentity, type InstanceIdStorage } from '../instanceId' /** * 一条端到端的接线:局域网浏览器从「解析实例 id」到「真的发出 hello」。 * * 修的就是这条链路:`setPage()` 翻页后会**再发一次 hello**(协议只有 hello 这一种上报方式), * 而局域网浏览器原先 displayKey / instanceId 都是空的 —— 服务端 `registerDisplay` 在 id 为空时 * **每次现生成一个** `display-<随机>`,于是切一次页就在注册表里多留一条该设备的离线残影 * (管理端列表里凭空多出一条离线条目,TTL 十分钟)。 * * 这里用真的 RealtimeClient + 真的 deviceLabel + 真的 instanceId 解析跑一遍:两条 hello 的 * instanceId 必须一模一样。纯逻辑测试测不出这件事(它们各自都对,接起来才出问题)。 */ class FakeSocket { static readonly CONNECTING = 0 static readonly OPEN = 1 static readonly CLOSED = 3 static instances: FakeSocket[] = [] readyState = FakeSocket.CONNECTING onopen: (() => void) | null = null onmessage: ((event: { data: unknown }) => void) | null = null onerror: (() => void) | null = null onclose: ((event: { code: number }) => void) | null = null readonly sent: string[] = [] constructor() { FakeSocket.instances.push(this) } send(data: string): void { this.sent.push(data) } close(): void { this.readyState = FakeSocket.CLOSED this.onclose?.({ code: 1006 }) } open(): void { this.readyState = FakeSocket.OPEN this.onopen?.() } hellos(): Array> { return this.sent .map((raw) => JSON.parse(raw) as { event: string; data: Record }) .filter((message) => message.event === 'hello') .map((message) => message.data) } } function fakeStorage(initial: Record = {}): InstanceIdStorage { const map = new Map(Object.entries(initial)) return { getItem: (key) => map.get(key) ?? null, setItem: (key, value) => void map.set(key, value), } } /** 安卓手机打开局域网地址:没有 instance / screen,只有 UA 与屏幕信息。 */ const ANDROID_SOURCE: DeviceLabelSource = { userAgent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36', screen: { width: 1080, height: 2400 }, devicePixelRatio: 3, } function connectLanBrowser( storage: InstanceIdStorage, source: DeviceLabelSource, ): { socket: FakeSocket; client: RealtimeClient } { ;(globalThis as { WebSocket?: unknown }).WebSocket = FakeSocket FakeSocket.instances = [] const identity = resolveDisplayIdentity({ instance: null, screen: null }, storage) const label = deviceLabel(source) const client = new RealtimeClient({ role: 'display', url: 'ws://test/ws', displayKey: identity.displayKey, instanceId: identity.instanceId, device: label.device, deviceRaw: label.deviceRaw, page: 'standings', }) client.connect() const socket = FakeSocket.instances[0] assert.ok(socket) socket.open() return { socket, client } } test('局域网浏览器:切页补发的两条 hello 用同一个实例 id(不再多出离线残影)', () => { const { socket, client } = connectLanBrowser(fakeStorage(), ANDROID_SOURCE) client.setPage('team-medals') const hellos = socket.hellos() assert.equal(hellos.length, 2, '切页后必须补发一条 hello(服务端注册表里的当前页要跟上)') assert.equal(typeof hellos[0]?.instanceId, 'string') assert.match(String(hellos[0]?.instanceId), /^browser-[0-9a-f]{8}$/) assert.equal(hellos[1]?.instanceId, hellos[0]?.instanceId, '两条 hello 的实例 id 必须一致') assert.equal(hellos[1]?.displayKey, hellos[0]?.displayKey) assert.equal(hellos[0]?.device, '安卓 10 · Chrome 120 · 1080×2400 @3x · 手机') assert.equal(hellos[1]?.page, 'team-medals') client.close() }) test('刷新 / 重连(同一个浏览器)沿用 localStorage 里那一个实例 id', () => { const storage = fakeStorage() const first = connectLanBrowser(storage, ANDROID_SOURCE) const firstName = first.socket.hellos()[0]?.instanceId first.client.close() // 重新打开页面:URL 还是没参数,但存储里有上一次那个 id。 const second = connectLanBrowser(storage, ANDROID_SOURCE) assert.equal(second.socket.hellos()[0]?.instanceId, firstName) second.client.close() // 重新连接也仍然读同一个键。 assert.equal(typeof storage.getItem(DISPLAY_INSTANCE_STORAGE_KEY), 'string') }) test('隐私模式(存储不可用):不报实例 id,行为退回今天的样子', () => { const brokenStorage: InstanceIdStorage = { getItem: () => { throw new Error('SecurityError') }, setItem: () => { throw new Error('SecurityError') }, } const { socket, client } = connectLanBrowser(brokenStorage, ANDROID_SOURCE) const hello = socket.hellos()[0] assert.equal(hello?.instanceId, undefined, '存不下就不报,交给服务端生成') assert.equal(hello?.displayKey, undefined) // 标签照常有:存储问题不该影响设备信息。 assert.equal(hello?.device, '安卓 10 · Chrome 120 · 1080×2400 @3x · 手机') client.close() }) test('Console 打开的窗口:实例 id 与屏号仍然来自 URL,一个字不变', () => { const identity = resolveDisplayIdentity({ instance: 'LAPTOP-1-2528732444', screen: '2' }, fakeStorage()) assert.equal(identity.displayKey, 'LAPTOP-1-2528732444') assert.equal(identity.instanceId, 'LAPTOP-1-2528732444') })