/** * OmniCard AI (CRMmp) - 展会团队战报测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/team/overview 设计编写 */ import * as fs from 'fs'; import * as path from 'path'; const OVERVIEW_PAGE = path.resolve( __dirname, '../../../mpuniapp/pages/owner/team/overview.uvue' ); const OVERVIEW_UTS = path.resolve( __dirname, '../../../mpuniapp/pages/owner/team/useOverview.uts' ); type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived'; type LeadEntity = { id: string | number; cardId: string | number; name: string; company?: string; status: LeadStatus; intentScore: number; source: 'nfc' | 'qr' | 'link' | 'ai_chat'; }; type RadarTrack = { id: string | number; cardId: string | number; visitorId: string; touchType: 'nfc' | 'qr' | 'link'; stayDurationMs: number; scrollDepth: number; viewedBlocks: string[]; country?: string; city?: string; }; type OwnerPageQuery = { page: number; size: number; cardId?: string | number; }; type OwnerPageData = { list: T[]; pagination: { page: number; size: number; total: number; }; }; type OverviewSummary = { totalLeads: number; hotCount: number; totalTracks: number; topCountry: string; }; type LoadOverviewApi = { fetchOwnerLeadPage?: ( query: OwnerPageQuery ) => Promise>; fetchOwnerRadarPage?: ( query: OwnerPageQuery ) => Promise>; }; type LoadOverviewResult = { leads: LeadEntity[]; tracks: RadarTrack[]; summary: OverviewSummary; empty: boolean; errorMessage: string; }; type LoadOverviewInput = { page?: number; size?: number; cardId?: string | number; }; type UseOverviewModule = { summarize: (leads: LeadEntity[], tracks: RadarTrack[]) => OverviewSummary; loadOverview: ( input?: LoadOverviewInput, api?: LoadOverviewApi ) => Promise; }; const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/; function buildLead(overrides?: Partial): LeadEntity { return { id: 21, cardId: 8, name: 'Alex Vance', company: 'Nexus Tech', status: 'new', intentScore: 72, source: 'nfc', ...overrides, }; } function buildTrack(overrides?: Partial): RadarTrack { return { id: 81, cardId: 8, visitorId: 'vis_ae_1201', country: 'United Arab Emirates', city: 'Dubai', touchType: 'nfc', stayDurationMs: 45000, scrollDepth: 72, viewedBlocks: ['hero', 'case'], ...overrides, }; } function buildPage( list: T[], pagination?: Partial['pagination']> ): OwnerPageData { return { list, pagination: { page: 1, size: 20, total: list.length, ...pagination, }, }; } function loadUseOverview(): UseOverviewModule { // eslint-disable-next-line @typescript-eslint/no-require-imports return require(OVERVIEW_UTS) as UseOverviewModule; } describe('OmniCard AI 展会团队战报 (useOverview.uts)', () => { beforeEach(() => { jest.resetModules(); (global as { uni?: Record }).uni = { getLocale: () => 'zh-CN', showToast: jest.fn(), navigateTo: jest.fn(), }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); describe('1. summarize 统计正确', () => { it('1.1 汇总线索总数、高意向数、足迹数与主要国家', () => { const { summarize } = loadUseOverview(); const leads: LeadEntity[] = [ buildLead({ id: 1, status: 'hot' }), buildLead({ id: 2, status: 'new' }), buildLead({ id: 3, status: 'hot' }), buildLead({ id: 4, status: 'won' }), ]; const tracks: RadarTrack[] = [ buildTrack({ id: 11, country: 'Germany' }), buildTrack({ id: 12, country: 'United Arab Emirates' }), buildTrack({ id: 13, country: 'United Arab Emirates' }), buildTrack({ id: 14, country: 'Germany' }), buildTrack({ id: 15, country: 'United Arab Emirates' }), ]; const summary = summarize(leads, tracks); expect(summary.totalLeads).toBe(4); expect(summary.hotCount).toBe(2); expect(summary.totalTracks).toBe(5); expect(summary.topCountry).toBe('United Arab Emirates'); }); it('1.2 空列表归零,忽略空白国家,平局取先出现的国家', () => { const { summarize } = loadUseOverview(); const empty = summarize([], []); expect(empty.totalLeads).toBe(0); expect(empty.hotCount).toBe(0); expect(empty.totalTracks).toBe(0); expect(empty.topCountry).toBe(''); const tied = summarize( [buildLead({ id: 9, status: 'contacted' })], [ buildTrack({ id: 21, country: ' ' }), buildTrack({ id: 22, country: 'Japan' }), buildTrack({ id: 23, country: 'France' }), buildTrack({ id: 24, country: undefined }), buildTrack({ id: 25, country: 'France' }), buildTrack({ id: 26, country: 'Japan' }), ] ); expect(tied.totalLeads).toBe(1); expect(tied.hotCount).toBe(0); expect(tied.totalTracks).toBe(6); expect(tied.topCountry).toBe('Japan'); }); }); describe('2. loadOverview 调两个 API', () => { it('2.1 并行注入 fetchOwnerLeadPage 与 fetchOwnerRadarPage,默认 size 为 20', async () => { const { loadOverview } = loadUseOverview(); const leads = [ buildLead({ id: 1, status: 'hot', name: 'Mina Cole' }), buildLead({ id: 2, status: 'new', name: 'Omar Said' }), ]; const tracks = [ buildTrack({ id: 31, country: 'Singapore' }), buildTrack({ id: 32, country: 'Singapore' }), buildTrack({ id: 33, country: 'Spain' }), ]; let leadStarted = false; let radarStarted = false; let leadSawRadar = false; let radarSawLead = false; const fetchOwnerLeadPage = jest.fn( async (query: OwnerPageQuery): Promise> => { leadStarted = true; leadSawRadar = radarStarted; expect(query.page).toBe(1); expect(query.size).toBe(20); return buildPage(leads, { page: 1, size: 20, total: 2 }); } ); const fetchOwnerRadarPage = jest.fn( async (query: OwnerPageQuery): Promise> => { radarStarted = true; radarSawLead = leadStarted; expect(query.page).toBe(1); expect(query.size).toBe(20); return buildPage(tracks, { page: 1, size: 20, total: 3 }); } ); const result = await loadOverview( { page: 1, size: 20 }, { fetchOwnerLeadPage, fetchOwnerRadarPage } ); expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1); expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1); expect(leadSawRadar || radarSawLead).toBe(true); expect(result.leads).toHaveLength(2); expect(result.tracks).toHaveLength(3); expect(result.summary.totalLeads).toBe(2); expect(result.summary.hotCount).toBe(1); expect(result.summary.totalTracks).toBe(3); expect(result.summary.topCountry).toBe('Singapore'); expect(result.empty).toBe(false); expect(result.errorMessage).toBe(''); }); it('2.2 无入参时仍调用两个 API,且无注入依赖时走默认实现', async () => { const { loadOverview } = loadUseOverview(); const fetchOwnerLeadPage = jest.fn( async (): Promise> => buildPage([]) ); const fetchOwnerRadarPage = jest.fn( async (): Promise> => buildPage([]) ); const result = await loadOverview(undefined, { fetchOwnerLeadPage, fetchOwnerRadarPage, }); expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1); expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1); expect(fetchOwnerLeadPage).toHaveBeenCalledWith( expect.objectContaining({ page: 1, size: 20 }) ); expect(fetchOwnerRadarPage).toHaveBeenCalledWith( expect.objectContaining({ page: 1, size: 20 }) ); expect(result.empty).toBe(true); expect(result.errorMessage).toBe(''); expect(result.summary.totalLeads).toBe(0); expect(result.summary.totalTracks).toBe(0); }); it('2.3 任一接口失败写入错误,不伪装成功汇总', async () => { const { loadOverview } = loadUseOverview(); const fetchOwnerLeadPage = jest.fn( async (): Promise> => { throw new Error('lead page unavailable'); } ); const fetchOwnerRadarPage = jest.fn( async (): Promise> => buildPage([buildTrack()]) ); const result = await loadOverview( { size: 20 }, { fetchOwnerLeadPage, fetchOwnerRadarPage } ); expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1); expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1); expect(result.leads).toEqual([]); expect(result.tracks).toEqual([]); expect(result.summary.totalLeads).toBe(0); expect(result.summary.hotCount).toBe(0); expect(result.summary.totalTracks).toBe(0); expect(result.summary.topCountry).toBe(''); expect(result.empty).toBe(false); expect(result.errorMessage).toBe('lead page unavailable'); }); }); }); describe('OmniCard AI 展会团队战报页契约 (overview.uvue)', () => { it('页面存在且含数字摘要、list-view 与刷新主 CTA', () => { expect(fs.existsSync(OVERVIEW_PAGE)).toBe(true); expect(fs.existsSync(OVERVIEW_UTS)).toBe(true); const pageSource = fs.readFileSync(OVERVIEW_PAGE, 'utf8'); const logicSource = fs.readFileSync(OVERVIEW_UTS, 'utf8'); const combined = `${pageSource}\n${logicSource}`; expect(pageSource.includes('script setup lang="uts"')).toBe(true); expect(pageSource.includes(' 5') || logicSource.includes('length > 5')).toBe( true ); expect(combined.includes('summarize')).toBe(true); expect(combined.includes('loadOverview')).toBe(true); expect(combined.includes('fetchOwnerLeadPage')).toBe(true); expect(combined.includes('fetchOwnerRadarPage')).toBe(true); expect(logicSource.includes('Promise.all')).toBe(true); expect(pageSource.includes('getOmniText')).toBe(true); expect(combined.includes('crm.leads')).toBe(true); expect(combined.includes('radar.visitor_trace')).toBe(true); expect(pageSource.includes('primary-cta-btn')).toBe(true); expect(pageSource.includes('#0B5CFF')).toBe(true); expect(pageSource.includes('#F7F8FA')).toBe(true); expect(pageSource.includes('#101828')).toBe(true); expect(pageSource.includes('#667085')).toBe(true); expect(pageSource.includes('#E3E7ED')).toBe(true); expect(pageSource.includes('#FFFFFF')).toBe(true); }); it('无 emoji、无 any,模板无硬编码中文主文案,长列表不用 view v-for', () => { const pageSource = fs.readFileSync(OVERVIEW_PAGE, 'utf8'); const logicSource = fs.readFileSync(OVERVIEW_UTS, 'utf8'); const testSource = fs.readFileSync(__filename, 'utf8'); const template = pageSource.split(']*v-for/.test(template)).toBe(false); expect(/[\u4e00-\u9fff]/.test(template)).toBe(false); expect(pageSource.includes('innerAudioContext') || pageSource.includes('autoplay')).toBe( false ); }); });