/** * 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; }; }; type MockPortalCard = { id: string; name: string; title: string; company: string; status: string; bentoBlocks: MockBentoBlock[]; }; type UniRequestSuccessRes = { statusCode: number; data: unknown; header: Record; }; type UniRequestOption = { url: string; method?: string; data?: Record; 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; buildExchangePath: (cardId: string) => string; openCaseExchange: (cardId: string) => void; goCaseBack: () => void; }; 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 }).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); }); }); });