文件
OmniCard/backend/test/mpuniapp/case-detail.test.ts

451 行
14 KiB
TypeScript

/**
* OmniCard AI (CRMmp) - mpuniapp 案例作品集详情测试套件 (TDD Red -> Green)
*
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD pages/portal/case-detail 设计编写
*/
type CaseKpi = {
label: string;
value: string;
};
type CaseDetailState = {
cardId: string;
blockId: string;
loading: boolean;
empty: boolean;
errorMessage: string;
title: string;
content: string;
images: string[];
kpis: CaseKpi[];
pdfUrl: string;
youtubeUrl: string;
};
type CaseDetailPageOptions = {
cardId?: string;
blockId?: string;
id?: string;
};
type MockBentoBlock = {
id: number | string;
cardId: string;
type: string;
colSpan: number;
rowSpan: number;
sortOrder: number;
title?: string;
content?: string;
mediaUrl?: string;
metaJson?:
| string
| {
images?: string[];
kpis?: CaseKpi[];
metrics?: CaseKpi[];
pdfUrl?: string;
youtubeUrl?: string;
client?: string;
industry?: string;
region?: string;
challenge?: string;
solution?: string;
[key: string]: unknown;
};
};
type MockPortalCard = {
id: string;
name: string;
title: string;
company: string;
status: string;
bentoBlocks: MockBentoBlock[];
};
type UniRequestSuccessRes = {
statusCode: number;
data: unknown;
header: Record<string, string>;
};
type UniRequestOption = {
url: string;
method?: string;
data?: Record<string, string>;
success?: (res: UniRequestSuccessRes) => void;
fail?: (err: { errMsg: string }) => void;
complete?: () => void;
};
type UseCaseDetailModule = {
createCaseDetailState: () => CaseDetailState;
applyCaseDetailOptions: (
state: CaseDetailState,
options: CaseDetailPageOptions | null
) => boolean;
pickCaseBlock: (
card: MockPortalCard | null,
blockId: string
) => MockBentoBlock | null;
applyPortalCase: (state: CaseDetailState, card: MockPortalCard | null) => boolean;
loadCaseDetail: (state: CaseDetailState) => Promise<boolean>;
buildExchangePath: (cardId: string) => string;
openCaseExchange: (cardId: string) => void;
goCaseBack: () => void;
copyCaseExternalLink: (url: string) => boolean;
resolveCaseDetailText: (key: string) => string;
};
function loadUseCaseDetail(): UseCaseDetailModule {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('../../../mpuniapp/pages/portal/useCaseDetail.uts') as UseCaseDetailModule;
}
function buildMockPortal(): MockPortalCard {
return {
id: 'card-101',
name: 'Alex Vance',
title: 'Managing Director & Global Partner',
company: 'Nexus Tech Global Ltd',
status: 'published',
bentoBlocks: [
{
id: 1,
cardId: 'card-101',
type: 'hero',
colSpan: 2,
rowSpan: 1,
sortOrder: 0,
title: 'Hero Block',
},
{
id: 2,
cardId: 'card-101',
type: 'case',
colSpan: 2,
rowSpan: 1,
sortOrder: 1,
title: 'Middle East Energy Hub',
content: 'Built scalable renewable energy microgrid in UAE.',
mediaUrl: 'https://cdn.omnicard.ai/case/cover.jpg',
metaJson: {
images: [
'https://cdn.omnicard.ai/case/1.jpg',
'https://cdn.omnicard.ai/case/2.jpg',
],
kpis: [{ label: 'ROI', value: '32%' }],
pdfUrl: 'https://cdn.omnicard.ai/case/brief.pdf',
youtubeUrl: 'https://www.youtube.com/watch?v=abc123',
},
},
{
id: 3,
cardId: 'card-101',
type: 'article',
colSpan: 1,
rowSpan: 1,
sortOrder: 2,
title: 'The Future of Cross-Border Trade',
},
],
};
}
describe('OmniCard AI 用户端案例详情业务逻辑测试 (useCaseDetail.uts)', () => {
let lastRequestOptions: UniRequestOption | null;
let navigateToUrl: string;
let navigateBackCount: number;
let mockRequestResponse: {
statusCode?: number;
data?: {
code?: number;
message?: string;
data?: MockPortalCard | null;
};
failError?: { errMsg: string };
};
beforeEach(() => {
lastRequestOptions = null;
navigateToUrl = '';
navigateBackCount = 0;
mockRequestResponse = {
statusCode: 200,
data: {
code: 1000,
message: 'success',
data: buildMockPortal(),
},
};
(global as { uni?: Record<string, unknown> }).uni = {
getStorageSync: () => '',
setStorageSync: () => undefined,
showToast: () => undefined,
navigateTo: (options: { url: string }) => {
navigateToUrl = options.url;
},
navigateBack: () => {
navigateBackCount += 1;
},
request: (options: UniRequestOption) => {
lastRequestOptions = 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: () => undefined,
};
},
};
});
afterEach(() => {
delete (global as { uni?: unknown }).uni;
});
describe('1. 从门户聚合数据选出 case 块', () => {
it('1.1 能从 mock portal 数据选出 id 匹配且 type 为 case 的块', () => {
const { pickCaseBlock } = loadUseCaseDetail();
const card = buildMockPortal();
const block = pickCaseBlock(card, '2');
expect(block).not.toBeNull();
expect(block?.type).toBe('case');
expect(block?.title).toBe('Middle East Energy Hub');
expect(block?.content).toContain('renewable');
});
it('1.2 applyPortalCase 写入标题、正文、多图、KPI 与外链', () => {
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
loadUseCaseDetail();
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
const found = applyPortalCase(state, buildMockPortal());
expect(found).toBe(true);
expect(state.empty).toBe(false);
expect(state.title).toBe('Middle East Energy Hub');
expect(state.content).toBe(
'Built scalable renewable energy microgrid in UAE.'
);
expect(state.images).toEqual([
'https://cdn.omnicard.ai/case/1.jpg',
'https://cdn.omnicard.ai/case/2.jpg',
]);
expect(state.kpis).toEqual([{ label: 'ROI', value: '32%' }]);
expect(state.pdfUrl).toBe('https://cdn.omnicard.ai/case/brief.pdf');
expect(state.youtubeUrl).toBe('https://www.youtube.com/watch?v=abc123');
});
it('1.3 无 images 时回退 mediaUrl 单图,metrics 可作为 KPI', () => {
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
loadUseCaseDetail();
const card = buildMockPortal();
card.bentoBlocks[1].mediaUrl = 'https://cdn.omnicard.ai/case/cover.jpg';
card.bentoBlocks[1].metaJson = {
metrics: [{ label: 'Capacity', value: '120MW' }],
};
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
applyPortalCase(state, card);
expect(state.images).toEqual(['https://cdn.omnicard.ai/case/cover.jpg']);
expect(state.kpis).toEqual([{ label: 'Capacity', value: '120MW' }]);
});
it('1.4 metaJson 为字符串且 metrics 使用 name 时仍能解析', () => {
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
loadUseCaseDetail();
const card = buildMockPortal();
card.bentoBlocks[1].metaJson = JSON.stringify({
metrics: [{ name: 'Uplift', value: 18 }],
});
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
applyPortalCase(state, card);
expect(state.kpis).toEqual([{ label: 'Uplift', value: '18' }]);
});
});
describe('2. 缺 block 返空', () => {
it('2.1 找不到 blockId 时 pickCaseBlock 返回 null', () => {
const { pickCaseBlock } = loadUseCaseDetail();
const block = pickCaseBlock(buildMockPortal(), '999');
expect(block).toBeNull();
});
it('2.2 id 匹配但 type 不是 case 时同样返空', () => {
const { pickCaseBlock } = loadUseCaseDetail();
const article = pickCaseBlock(buildMockPortal(), '3');
const hero = pickCaseBlock(buildMockPortal(), '1');
expect(article).toBeNull();
expect(hero).toBeNull();
});
it('2.3 applyPortalCase 缺块时 empty=true 且展示字段清空', () => {
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
loadUseCaseDetail();
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '999' });
const found = applyPortalCase(state, buildMockPortal());
expect(found).toBe(false);
expect(state.empty).toBe(true);
expect(state.loading).toBe(false);
expect(state.title).toBe('');
expect(state.content).toBe('');
expect(state.images).toEqual([]);
expect(state.kpis).toEqual([]);
});
});
describe('3. onLoad 拉取门户详情', () => {
it('3.1 loadCaseDetail 通过 fetchPortalCard 选出 case 块', async () => {
const { createCaseDetailState, applyCaseDetailOptions, loadCaseDetail } =
loadUseCaseDetail();
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
const found = await loadCaseDetail(state);
expect(found).toBe(true);
expect(lastRequestOptions).not.toBeNull();
expect(lastRequestOptions?.url).toContain('/open/card/portal/detail');
expect(state.empty).toBe(false);
expect(state.title).toBe('Middle East Energy Hub');
});
it('3.2 缺 block 时 loadCaseDetail 返空', async () => {
const { createCaseDetailState, applyCaseDetailOptions, loadCaseDetail } =
loadUseCaseDetail();
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '888' });
const found = await loadCaseDetail(state);
expect(found).toBe(false);
expect(state.empty).toBe(true);
expect(state.title).toBe('');
});
it('3.3 缺少 cardId 或 blockId 时不请求并返空', async () => {
const { createCaseDetailState, applyCaseDetailOptions, loadCaseDetail } =
loadUseCaseDetail();
const state = createCaseDetailState();
applyCaseDetailOptions(state, {});
const found = await loadCaseDetail(state);
expect(found).toBe(false);
expect(state.empty).toBe(true);
expect(lastRequestOptions).toBeNull();
});
});
describe('4. 咨询互换 CTA', () => {
it('4.1 主 CTA 跳转 /pages/portal/exchange?cardId=', () => {
const { buildExchangePath, openCaseExchange } = loadUseCaseDetail();
expect(buildExchangePath('card-101')).toBe(
'/pages/portal/exchange?cardId=card-101'
);
openCaseExchange('card-101');
expect(navigateToUrl).toBe('/pages/portal/exchange?cardId=card-101');
});
it('4.2 空状态返回调用 navigateBack', () => {
const { goCaseBack } = loadUseCaseDetail();
goCaseBack();
expect(navigateBackCount).toBe(1);
});
});
describe('5. Bento Grid 模块化结构化数据与国际化解析', () => {
it('5.1 支持从 metaJson 解析 client, industry, region, challenge, solution', () => {
const { createCaseDetailState, applyCaseDetailOptions, applyPortalCase } =
loadUseCaseDetail();
const card = buildMockPortal();
card.bentoBlocks[1].metaJson = {
client: 'Global Logistics Corp',
industry: 'Supply Chain IoT',
region: 'Middle East & North Africa',
challenge: 'High latency in cross-border tracking and data silos.',
solution: 'Deployed dual-frequency NFC sensors and cloud-native mesh.',
kpis: [{ label: 'On-time Delivery', value: '99.8%' }],
};
const state = createCaseDetailState();
applyCaseDetailOptions(state, { cardId: 'card-101', blockId: '2' });
applyPortalCase(state, card);
expect((state as any).client).toBe('Global Logistics Corp');
expect((state as any).industry).toBe('Supply Chain IoT');
expect((state as any).region).toBe('Middle East & North Africa');
expect((state as any).challenge).toBe(
'High latency in cross-border tracking and data silos.'
);
expect((state as any).solution).toBe(
'Deployed dual-frequency NFC sensors and cloud-native mesh.'
);
expect(state.kpis).toEqual([{ label: 'On-time Delivery', value: '99.8%' }]);
});
it('5.2 resolveCaseDetailText 正确解析新增的 Bento 模组词条', () => {
const { resolveCaseDetailText } = loadUseCaseDetail();
expect(resolveCaseDetailText('badge')).toBeDefined();
expect(resolveCaseDetailText('overview')).toBeDefined();
expect(resolveCaseDetailText('challenge')).toBeDefined();
expect(resolveCaseDetailText('solution')).toBeDefined();
expect(resolveCaseDetailText('kpis')).toBeDefined();
expect(resolveCaseDetailText('resources')).toBeDefined();
expect(resolveCaseDetailText('pdfAction')).toBeDefined();
expect(resolveCaseDetailText('videoAction')).toBeDefined();
});
it('5.3 copyCaseExternalLink 触发 uni.setClipboardData 与 Toast 提示', () => {
let clipboardText = '';
let toastCalled = false;
(global as { uni?: Record<string, unknown> }).uni = {
setClipboardData: (opts: { data: string; success?: () => void }) => {
clipboardText = opts.data;
opts.success?.();
},
showToast: () => {
toastCalled = true;
},
};
const { copyCaseExternalLink } = loadUseCaseDetail();
const res = copyCaseExternalLink('https://cdn.omnicard.ai/doc.pdf');
expect(res).toBe(true);
expect(clipboardText).toBe('https://cdn.omnicard.ai/doc.pdf');
expect(toastCalled).toBe(true);
const emptyRes = copyCaseExternalLink('');
expect(emptyRes).toBe(false);
});
});
});