文件
OmniCard/backend/test/mpuniapp/portal-cache.test.ts

339 行
11 KiB
TypeScript

/**
* 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<string, string>;
success?: (res: { statusCode: number; data: unknown; header: Record<string, string> }) => void;
fail?: (err: Error) => void;
complete?: () => void;
}) => { abort: () => void };
}
describe('移动微官网离线缓存与极速预加载套件 (useCache.uts & usePortal.uts)', () => {
let storageMap: Map<string, string>;
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();
});
});
});