/** * 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 TeamRankItem = { memberId: number; memberName: string; leads: number; touches: number; score: number; rank: number; }; type LoadOverviewApi = { fetchOwnerLeadPage?: ( query: OwnerPageQuery ) => Promise>; fetchOwnerRadarPage?: ( query: OwnerPageQuery ) => Promise>; fetchTeamLiveRank?: (eventId?: string) => Promise; fetchCurrentEvent?: () => Promise<{ code?: string } | null>; }; type LoadOverviewResult = { leads: LeadEntity[]; tracks: RadarTrack[]; ranks: TeamRankItem[]; summary: OverviewSummary; empty: boolean; errorMessage: string; }; type LoadOverviewInput = { page?: number; size?: number; cardId?: string | number; }; type OverviewState = { leads: LeadEntity[]; tracks: RadarTrack[]; summary: OverviewSummary; loading: boolean; empty: boolean; errorMessage: string; }; type UseOverviewModule = { summarize: (leads: LeadEntity[], tracks: RadarTrack[]) => OverviewSummary; loadOverview: ( input?: LoadOverviewInput, api?: LoadOverviewApi ) => Promise; createOverviewState: () => OverviewState; refreshOverview: ( state: OverviewState, 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'); }); it('2.4 注入 liveRank 时写入现场排行', async () => { const { loadOverview } = loadUseOverview(); const result = await loadOverview( { page: 1, size: 20 }, { fetchOwnerLeadPage: async () => buildPage([]), fetchOwnerRadarPage: async () => buildPage([]), fetchCurrentEvent: async () => ({ code: 'CES26A' }), fetchTeamLiveRank: async (eventId?: string) => { expect(eventId).toBe('CES26A'); return [ { memberId: 11, memberName: 'Alex Rivera', leads: 3, touches: 12, score: 42, rank: 1, }, ]; }, } ); expect(result.ranks).toHaveLength(1); expect(result.ranks[0].memberName).toBe('Alex Rivera'); }); }); describe('3. refreshOverview 触觉震动与状态刷新', () => { it('3.1 刷新成功时触发 uni.vibrateShort 触觉震动反馈', async () => { const { refreshOverview, createOverviewState } = loadUseOverview(); const state = createOverviewState(); const fetchOwnerLeadPage = jest.fn(async () => buildPage([buildLead()])); const fetchOwnerRadarPage = jest.fn(async () => buildPage([buildTrack()])); const vibrateMock = jest.fn(); (global as { uni?: Record }).uni!.vibrateShort = vibrateMock; await refreshOverview(state, { fetchOwnerLeadPage, fetchOwnerRadarPage }); expect(vibrateMock).toHaveBeenCalledTimes(1); expect(state.loading).toBe(false); expect(state.leads.length).toBe(1); }); it('3.2 刷新失败写入错误态,不填迪拜示例数、不假装成功', async () => { const { refreshOverview, createOverviewState } = loadUseOverview(); const state = createOverviewState(); state.summary = { totalLeads: 99, hotCount: 9, totalTracks: 88, topCountry: 'United Arab Emirates', }; state.leads = [buildLead()]; state.tracks = [buildTrack()]; state.errorMessage = ''; const fetchOwnerLeadPage = jest.fn(async (): Promise> => { throw new Error('network down'); }); const fetchOwnerRadarPage = jest.fn( async (): Promise> => buildPage([buildTrack()]) ); const vibrateMock = jest.fn(); (global as { uni?: Record }).uni!.vibrateShort = vibrateMock; const result = await refreshOverview(state, { fetchOwnerLeadPage, fetchOwnerRadarPage, }); expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1); expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1); expect(result.errorMessage).toBe('network down'); expect(state.errorMessage).toBe('network down'); expect(state.leads).toEqual([]); expect(state.tracks).toEqual([]); expect(state.summary.totalLeads).toBe(0); expect(state.summary.hotCount).toBe(0); expect(state.summary.totalTracks).toBe(0); expect(state.summary.topCountry).toBe(''); expect(state.summary.topCountry).not.toBe('United Arab Emirates'); expect(vibrateMock).not.toHaveBeenCalled(); expect(state.loading).toBe(false); }); }); }); 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( false ); expect((pageSource.match(/ { const pageSource = fs.readFileSync(OVERVIEW_PAGE, 'utf8'); const logicSource = fs.readFileSync(OVERVIEW_UTS, 'utf8'); const template = pageSource.split('')[0] : ''; expect((pageSource.match(/primary-cta-btn/g) || []).length).toBeGreaterThanOrEqual(1); expect((template.match(/class="primary-cta-btn"/g) || []).length).toBe(1); expect((template.match(/