/** * OmniCard AI (CRMmp) - 用户端离线缓存与断网重放队列单元测试 * 严格遵循 TDD 测试驱动规范与瑞士国际主义极简原则 */ describe('Point 41: 移动端弱网离线视觉指示器与断网重放队列 (Offline Queue)', () => { let storageMap: Record; let mod: any; beforeEach(() => { storageMap = {}; // 模拟全局 uni-app storage 与网络 API (global as any).uni = { getStorageSync: jest.fn((key: string) => storageMap[key] || ''), setStorageSync: jest.fn((key: string, val: string) => { storageMap[key] = String(val); }), removeStorageSync: jest.fn((key: string) => { delete storageMap[key]; }), onNetworkStatusChange: jest.fn(), }; // @ts-ignore mod = require('../../../mpuniapp/common/offline-queue.uts'); mod.clearOfflineQueue(); mod.setNetworkStatus(true, 'wifi'); }); afterEach(() => { delete (global as any).uni; }); describe('1. 网络在线/断网状态检测与切换', () => { it('默认初始状态应为在线 (isOnline = true)', () => { expect(mod.isOnline()).toBe(true); const status = mod.getOfflineQueueStatus(); expect(status.isOnline).toBe(true); expect(status.networkType).toBe('wifi'); }); it('断网时状态切换为离线并输出正确的国际化横幅文案', () => { mod.setNetworkStatus(false, 'none'); expect(mod.isOnline()).toBe(false); const status = mod.getOfflineQueueStatus(); expect(status.isOnline).toBe(false); expect(status.networkType).toBe('none'); // 中文与英文横幅文案 const zhBanner = mod.formatOfflineBannerText('zh', 2); expect(zhBanner).toContain('网络已断开'); expect(zhBanner).toContain('2 条暂存待同步'); const enBanner = mod.formatOfflineBannerText('en', 3); expect(enBanner).toContain('Offline'); expect(enBanner).toContain('3 pending'); const arBanner = mod.formatOfflineBannerText('ar', 1); expect(arBanner).toContain('غير متصل'); }); }); describe('2. 离线动作暂存入队 (enqueueOfflineAction)', () => { it('应能正确保存离线留资或轨迹提交动作至本地队列', () => { const action = { id: 'action-001', type: 'LEAD_SUBMIT', url: '/card/lead/submit', method: 'POST', data: { name: 'Faisal Al-Otaibi', email: 'faisal@sauditech.sa' }, timestamp: Date.now(), }; const count = mod.enqueueOfflineAction(action); expect(count).toBe(1); const queue = mod.getOfflineQueue(); expect(queue.length).toBe(1); expect(queue[0].id).toBe('action-001'); expect(queue[0].type).toBe('LEAD_SUBMIT'); expect(queue[0].retryCount).toBe(0); }); it('支持多次入队并保持先进先出 (FIFO) 顺序', () => { mod.enqueueOfflineAction({ id: 'act-1', type: 'RADAR_TRACK', url: '/crm/radar/track', timestamp: 1000, }); mod.enqueueOfflineAction({ id: 'act-2', type: 'LEAD_SUBMIT', url: '/crm/lead/create', timestamp: 2000, }); const queue = mod.getOfflineQueue(); expect(queue.length).toBe(2); expect(queue[0].id).toBe('act-1'); expect(queue[1].id).toBe('act-2'); }); it('自动生成缺失的 action.id 与 timestamp', () => { mod.enqueueOfflineAction({ type: 'RADAR_TRACK', url: '/crm/radar/ping', }); const queue = mod.getOfflineQueue(); expect(queue.length).toBe(1); expect(queue[0].id).toBeDefined(); expect(queue[0].id.length).toBeGreaterThan(0); expect(queue[0].timestamp).toBeGreaterThan(0); }); }); describe('3. 网络恢复自动重放队列 (replayOfflineQueue)', () => { it('成功执行所有队列动作后应自动清空队列', async () => { mod.enqueueOfflineAction({ id: 'act-success-1', type: 'LEAD_SUBMIT', url: '/card/lead/submit', data: { test: 1 }, }); mod.enqueueOfflineAction({ id: 'act-success-2', type: 'RADAR_TRACK', url: '/crm/radar/track', data: { test: 2 }, }); expect(mod.getOfflineQueue().length).toBe(2); const executedIds: string[] = []; const result = await mod.replayOfflineQueue(async (item: any) => { executedIds.push(item.id); return true; }); expect(result.total).toBe(2); expect(result.success).toBe(2); expect(result.failed).toBe(0); expect(executedIds).toEqual(['act-success-1', 'act-success-2']); expect(mod.getOfflineQueue().length).toBe(0); }); it('重放部分失败时应增加重试次数并保留失败项在队列中', async () => { mod.enqueueOfflineAction({ id: 'act-ok', type: 'LEAD_SUBMIT', url: '/api/ok', }); mod.enqueueOfflineAction({ id: 'act-fail', type: 'LEAD_SUBMIT', url: '/api/fail', }); const result = await mod.replayOfflineQueue(async (item: any) => { if (item.id === 'act-fail') { return false; } return true; }); expect(result.total).toBe(2); expect(result.success).toBe(1); expect(result.failed).toBe(1); const remaining = mod.getOfflineQueue(); expect(remaining.length).toBe(1); expect(remaining[0].id).toBe('act-fail'); expect(remaining[0].retryCount).toBe(1); }); it('超过最大重试次数 (默认 3 次) 时应安全移除死信项,避免永久阻塞', async () => { mod.enqueueOfflineAction({ id: 'act-dead', type: 'DEAD_LETTER', url: '/api/dead', retryCount: 3, // 已达上限 }); const result = await mod.replayOfflineQueue(async () => false, 3); expect(result.total).toBe(1); expect(result.failed).toBe(1); expect(mod.getOfflineQueue().length).toBe(0); // 超限后被淘汰 }); }); });