import { TestBed } from '@angular/core/testing'; import { UnreadService } from './unread.service'; import { SessionService } from './session.service'; /** * 未读计数单测 —— **直接守护本次开发抓出的两个 bug** * * ① 「刚连接就有 39 条未读」:初次 `/sync` 的历史回填被算成未读 * → 用 `catchingUp` 分界覆盖 * ② `data.liveEvent` 不能区分历史与实时(实测初次同步历史也为 `true`) * → 改用「初次同步完成分界 + 已读时间戳」双判据 * * 覆盖 SPEC FR-04 的未读口径与 FR-14(每域唯一 owner)。 */ describe('UnreadService', () => { let svc: UnreadService; let session: SessionService; beforeEach(() => { localStorage.clear(); TestBed.configureTestingModule({}); session = TestBed.inject(SessionService); svc = TestBed.inject(UnreadService); }); afterEach(() => localStorage.clear()); describe('catchingUp 分界(bug①回归)', () => { it('初次同步回填阶段:历史消息一律不计未读', () => { // 模拟 5 条历史按时间升序回填 —— 全部不应计数 const base = 1_700_000_000_000; for (let i = 0; i < 5; i++) { const counted = svc.onIncoming('agent-a', base + i * 1000, /*catchingUp*/ true); expect(counted).toBe(false); } expect(svc.countOf('agent-a')).toBe(0); }); it('分界之后:晚于基线的消息计入', () => { const base = 1_700_000_000_000; svc.onIncoming('agent-a', base, true); // 回填阶段建立基线 const counted = svc.onIncoming('agent-a', base + 5000, false); expect(counted).toBe(true); expect(svc.countOf('agent-a')).toBe(1); }); }); describe('时间戳口径(bug②回归)', () => { it('早于已读基线的消息不计(已读 / 重复投递)', () => { const base = 1_700_000_000_000; svc.onIncoming('a', base, true); expect(svc.onIncoming('a', base - 1000, false)).toBe(false); expect(svc.onIncoming('a', base, false)).toBe(false); // 等于基线也不计 expect(svc.countOf('a')).toBe(0); }); it('首次见到房间时只推进基线,不计未读', () => { expect(svc.onIncoming('new-agent', 1_700_000_000_000, false)).toBe(false); expect(svc.countOf('new-agent')).toBe(0); // 基线已建立,后续更新的消息才计 expect(svc.onIncoming('new-agent', 1_700_000_001_000, false)).toBe(true); expect(svc.countOf('new-agent')).toBe(1); }); it('离线期间积累的消息(晚于基线)全部计入', () => { const base = 1_700_000_000_000; svc.onIncoming('a', base, true); for (let i = 1; i <= 7; i++) svc.onIncoming('a', base + i * 1000, false); expect(svc.countOf('a')).toBe(7); }); }); describe('markRead', () => { it('清零该 agent 的未读并推进基线', () => { const base = 1_700_000_000_000; svc.onIncoming('a', base, true); svc.onIncoming('a', base + 1000, false); svc.onIncoming('a', base + 2000, false); expect(svc.countOf('a')).toBe(2); svc.markRead('a', base + 2000); expect(svc.countOf('a')).toBe(0); // 基线已推进 → 早于它的消息不再计 expect(svc.onIncoming('a', base + 1500, false)).toBe(false); }); it('只影响目标 agent,不误伤其它', () => { const t = 1_700_000_000_000; svc.onIncoming('a', t, true); svc.onIncoming('a', t + 1, false); svc.onIncoming('b', t, true); svc.onIncoming('b', t + 1, false); svc.markRead('a', t + 1); expect(svc.countOf('a')).toBe(0); expect(svc.countOf('b')).toBe(1); }); }); describe('total 与多 agent', () => { it('total 为各 agent 之和', () => { const t = 1_700_000_000_000; for (const id of ['a', 'b', 'c']) { svc.onIncoming(id, t, true); svc.onIncoming(id, t + 1, false); svc.onIncoming(id, t + 2, false); } expect(svc.countOf('a')).toBe(2); expect(svc.total()).toBe(6); }); }); describe('持久化(退出浏览器不丢)', () => { it('按用户隔离:不同用户互不串号', () => { // 用户1 session.setSession('t1', { id: 'u1', name: 'A' }); svc.reload(); const t = 1_700_000_000_000; svc.onIncoming('a', t, true); svc.onIncoming('a', t + 1, false); expect(svc.countOf('a')).toBe(1); // 换用户2 → 重新载入应为空 session.setSession('t2', { id: 'u2', name: 'B' }); svc.reload(); expect(svc.countOf('a')).toBe(0); // 切回用户1 → 计数恢复 session.setSession('t1', { id: 'u1', name: 'A' }); svc.reload(); expect(svc.countOf('a')).toBe(1); }); it('reset 清空并移除存储', () => { session.setSession('t1', { id: 'u1', name: 'A' }); const t = 1_700_000_000_000; svc.onIncoming('a', t, true); svc.onIncoming('a', t + 1, false); svc.reset(); expect(svc.countOf('a')).toBe(0); expect(svc.total()).toBe(0); }); }); describe('边界', () => { it('空 agentId 不计数', () => { expect(svc.onIncoming('', 1_700_000_000_000, false)).toBe(false); expect(svc.total()).toBe(0); }); }); });