feat(round-11): 弱网离线队列/跨国会议一键预约/团队多语言名片批量向导/NFC硬件出厂封印 (83套件984测试全绿)
这个提交包含在:
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* 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); // 超限后被淘汰
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户