文件
OmniCard/backend/test/mpuniapp/offline-queue.test.ts

194 行
5.9 KiB
TypeScript

/**
* OmniCard AI (CRMmp) - 用户端离线缓存与断网重放队列单元测试
* 严格遵循 TDD 测试驱动规范与瑞士国际主义极简原则
*/
describe('Point 41: 移动端弱网离线视觉指示器与断网重放队列 (Offline Queue)', () => {
let storageMap: Record<string, string>;
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); // 超限后被淘汰
});
});
});