/** * OmniCard AI (CRMmp) - 移动微官网离线 LocalStorage 极速预加载与骨架屏单测套件 (TDD) * 严格遵循 AGENTS.md 规范与高性能架构契约 */ export interface MockBentoBlock { id: number | string; cardId: number | string; type: string; colSpan: number; rowSpan: number; sortOrder: number; title?: string; content?: string; mediaUrl?: string; } export interface CardPortalDetail { id: string; slug?: string; name: string; title: string; company: string; timezone?: string; status: string; bentoBlocks: MockBentoBlock[]; } interface MockUniStorage { getStorageSync: (key: string) => string; setStorageSync: (key: string, val: string) => void; removeStorageSync: (key: string) => void; showToast: (options: { title: string; icon?: string }) => void; createInnerAudioContext: () => unknown; request: (options: { url: string; method?: string; data?: unknown; header?: Record; success?: (res: { statusCode: number; data: unknown; header: Record }) => void; fail?: (err: Error) => void; complete?: () => void; }) => { abort: () => void }; } describe('移动微官网离线缓存与极速预加载套件 (useCache.uts & usePortal.uts)', () => { let storageMap: Map; let mockRequestResponse: { statusCode?: number; data?: unknown; failError?: Error; }; const sampleCardData: CardPortalDetail = { id: 'card-dubai-888', slug: 'alex-dubai', name: 'Alex Vance', title: 'Managing Director', company: 'Nexus Tech Global Ltd', timezone: 'Dubai UTC+4', status: 'published', bentoBlocks: [ { id: 1, cardId: 'card-dubai-888', type: 'hero', colSpan: 2, rowSpan: 1, sortOrder: 0, title: 'Hero Profile', }, { id: 2, cardId: 'card-dubai-888', type: 'case', colSpan: 2, rowSpan: 1, sortOrder: 1, title: 'Middle East Solar Project', }, ], }; beforeEach(() => { storageMap = new Map(); mockRequestResponse = { statusCode: 200, data: { code: 1000, message: 'success', data: sampleCardData, }, }; const mockUni: MockUniStorage = { getStorageSync: (key: string): string => storageMap.get(key) || '', setStorageSync: (key: string, val: string): void => { storageMap.set(key, val); }, removeStorageSync: (key: string): void => { storageMap.delete(key); }, showToast: jest.fn(), createInnerAudioContext: () => ({ src: '', play: jest.fn(), stop: jest.fn(), pause: jest.fn(), destroy: jest.fn(), onPlay: jest.fn(), onEnded: jest.fn(), onError: jest.fn(), }), request: (options) => { setTimeout(() => { if (mockRequestResponse.failError) { options.fail?.(mockRequestResponse.failError); } else { options.success?.({ statusCode: mockRequestResponse.statusCode ?? 200, data: mockRequestResponse.data, header: {}, }); } options.complete?.(); }, 0); return { abort: jest.fn() }; }, }; (global as unknown as { uni: MockUniStorage }).uni = mockUni; }); afterEach(() => { delete (global as unknown as { uni?: MockUniStorage }).uni; }); describe('1. Composable useCache 基础能力与时效验证', () => { it('1.1 成功写入名片快照,并带版本号、时间戳与默认24h TTL', () => { // @ts-ignore const { setCachedPortal, getCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); const success = setCachedPortal('alex-dubai', sampleCardData); expect(success).toBe(true); const cached = getCachedPortal('alex-dubai'); expect(cached).not.toBeNull(); expect(cached?.slug).toBe('alex-dubai'); expect(cached?.version).toBe('1.0.0'); expect(cached?.data.name).toBe('Alex Vance'); expect(cached?.ttlMs).toBe(86400000); // 24小时 expect(typeof cached?.timestamp).toBe('number'); }); it('1.2 isCacheValid 正确校验有效与过期缓存', () => { // @ts-ignore const { isCacheValid } = require('../../../mpuniapp/pages/portal/useCache.uts'); const now = Date.now(); const validCache = { version: '1.0.0', slug: 'test-slug', timestamp: now - 3600000, // 1小时前 ttlMs: 86400000, data: sampleCardData, }; expect(isCacheValid(validCache)).toBe(true); const expiredCache = { version: '1.0.0', slug: 'test-slug', timestamp: now - 90000000, // 超过24小时 ttlMs: 86400000, data: sampleCardData, }; expect(isCacheValid(expiredCache)).toBe(false); expect(isCacheValid(null)).toBe(false); }); it('1.3 支持自定义 ttlMs 参数', () => { // @ts-ignore const { setCachedPortal, getCachedPortal, isCacheValid } = require('../../../mpuniapp/pages/portal/useCache.uts'); const shortTtlMs = 1000; setCachedPortal('fast-expire', sampleCardData, shortTtlMs); const cached = getCachedPortal('fast-expire'); expect(cached?.ttlMs).toBe(1000); expect(isCacheValid(cached)).toBe(true); // 模拟时间流逝超过 1s cached!.timestamp = Date.now() - 1500; expect(isCacheValid(cached)).toBe(false); }); it('1.4 本地损坏数据安全降级,返回 null 而不抛出异常', () => { // @ts-ignore const { getCachedPortal, setCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); // 模拟 JSON 损坏 storageMap.set('omni_portal_cache_corrupt-json', '{invalid_json'); expect(getCachedPortal('corrupt-json')).toBeNull(); // 模拟缺失必要字段 storageMap.set('omni_portal_cache_bad-structure', JSON.stringify({ wrong: 123 })); expect(getCachedPortal('bad-structure')).toBeNull(); // 空 key 保护 expect(getCachedPortal('')).toBeNull(); expect(setCachedPortal('', sampleCardData)).toBe(false); }); }); describe('2. 微官网集成加载策略 (usePortal with Cache)', () => { it('2.1 首次无缓存加载:开启骨架屏 loading=true,网络成功后呈现内容并持久化缓存', async () => { // @ts-ignore const { createPortalState, loadCardPortal } = require('../../../mpuniapp/pages/portal/usePortal.uts'); // @ts-ignore const { getCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); const state = createPortalState(); expect(state.loading).toBe(false); expect(state.card).toBeNull(); // 发起首次加载 const loadPromise = loadCardPortal(state, 'alex-dubai', 'qr'); // 无缓存,应进入骨架屏 loading 状态 expect(state.loading).toBe(true); const result = await loadPromise; expect(state.loading).toBe(false); expect(result).not.toBeNull(); expect(state.card?.id).toBe('card-dubai-888'); // 检查此时本地已被静默写入缓存 const cached = getCachedPortal('alex-dubai'); expect(cached).not.toBeNull(); expect(cached?.data.id).toBe('card-dubai-888'); }); it('2.2 命中有效缓存:首屏 0ms 呈现 (loading=false),并静默在后台更新最新数据', async () => { // @ts-ignore const { createPortalState, loadCardPortal } = require('../../../mpuniapp/pages/portal/usePortal.uts'); // @ts-ignore const { setCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); const initialCachedCard: CardPortalDetail = { ...sampleCardData, title: 'Old Title from Cache', }; // 先预埋有效缓存 setCachedPortal('alex-dubai', initialCachedCard); // 准备网络端将返回更新后的数据 const updatedNetworkCard: CardPortalDetail = { ...sampleCardData, title: 'Newly Updated Title from Network', }; mockRequestResponse.data = { code: 1000, message: 'success', data: updatedNetworkCard, }; const state = createPortalState(); // 执行 loadCardPortal const loadPromise = loadCardPortal(state, 'alex-dubai', 'link'); // 核心验证:首屏 0ms 秒开,loading 为 false,且立即获得缓存的名片数据 expect(state.loading).toBe(false); expect(state.card).not.toBeNull(); expect(state.card?.title).toBe('Old Title from Cache'); // 等待后台静默网络请求更新完成 const finalResult = await loadPromise; expect(state.loading).toBe(false); expect(finalResult?.title).toBe('Newly Updated Title from Network'); expect(state.card?.title).toBe('Newly Updated Title from Network'); }); it('2.3 缓存过期时:触发正常 loading 骨架屏并在请求完成后刷新缓存', async () => { // @ts-ignore const { createPortalState, loadCardPortal } = require('../../../mpuniapp/pages/portal/usePortal.uts'); // @ts-ignore const { setCachedPortal, getCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); // 预埋一个已过期的缓存 setCachedPortal('alex-dubai', sampleCardData, 100); const cached = getCachedPortal('alex-dubai'); cached!.timestamp = Date.now() - 500000; storageMap.set('omni_portal_cache_alex-dubai', JSON.stringify(cached)); const state = createPortalState(); const loadPromise = loadCardPortal(state, 'alex-dubai', 'link'); // 因为缓存已过期,不能直接作为有效首屏展示,应走骨架屏加载 expect(state.loading).toBe(true); const result = await loadPromise; expect(state.loading).toBe(false); expect(result).not.toBeNull(); }); it('2.4 网络异常或断网时:若存在有效缓存,平滑降级展示缓存数据而不报错误屏', async () => { // @ts-ignore const { createPortalState, loadCardPortal } = require('../../../mpuniapp/pages/portal/usePortal.uts'); // @ts-ignore const { setCachedPortal } = require('../../../mpuniapp/pages/portal/useCache.uts'); // 预埋有效缓存 setCachedPortal('alex-dubai', sampleCardData); // 模拟断网/服务故障 mockRequestResponse = { statusCode: 500, failError: new Error('Network Connection Lost'), }; const state = createPortalState(); const loadPromise = loadCardPortal(state, 'alex-dubai', 'link'); // 首屏直接拿到缓存 expect(state.loading).toBe(false); expect(state.card?.name).toBe('Alex Vance'); // 等待请求失败 const result = await loadPromise; expect(state.loading).toBe(false); // 数据保持不变,不被清空为 null,也不展示阻断性 errorMessage expect(state.card).not.toBeNull(); expect(state.card?.name).toBe('Alex Vance'); expect(state.errorMessage).toBe(''); expect(result).not.toBeNull(); }); }); });