diff --git a/backend/test/mpuniapp/owner-lead-detail.test.ts b/backend/test/mpuniapp/owner-lead-detail.test.ts index e2cc5b8..a2b4682 100644 --- a/backend/test/mpuniapp/owner-lead-detail.test.ts +++ b/backend/test/mpuniapp/owner-lead-detail.test.ts @@ -88,12 +88,24 @@ type UseLeadDetailModule = { existing: string | null | undefined, next: string | null | undefined ) => string; + appendStructuredNote: ( + existing: string | null | undefined, + next: string | null | undefined, + attachments?: string[] + ) => string; + formatWhatsAppUrl: (phone: string | null | undefined) => string; + generateAiIcebreaker: ( + lead: LeadEntity | null, + tracks: RadarTrack[], + locale?: string + ) => string; saveNote: ( input: { id: string | number; notes?: string; next: string; status?: LeadStatus; + attachments?: string[]; }, api?: SaveNoteApi ) => Promise; @@ -262,6 +274,42 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', () expect(result.notes).toBe('Met at GITEX\nAsked for quote'); expect(result.status).toBe('hot'); }); + + it('3.3 updateLeadStage 调用 updateOwnerLeadStatus 推进商机阶段', async () => { + const { updateLeadStage } = loadUseLeadDetail() as UseLeadDetailModule & { + updateLeadStage: ( + state: Record, + newStatus: LeadStatus, + api?: SaveNoteApi & { + updateOwnerLeadStatus?: ( + id: string | number, + status: LeadStatus + ) => Promise; + } + ) => Promise; + }; + const lead = buildLead({ status: 'new' }); + const state: Record = { + leadId: 21, + lead, + status: 'new', + saving: false, + errorMessage: '', + }; + const updateOwnerLeadStatus = jest.fn( + async (id: string | number, status: LeadStatus) => { + expect(id).toBe(21); + expect(status).toBe('hot'); + return buildLead({ id, status }); + } + ); + + const ok = await updateLeadStage(state, 'hot', { updateOwnerLeadStatus }); + expect(ok).toBe(true); + expect(state.status).toBe('hot'); + expect(state.saving).toBe(false); + expect(updateOwnerLeadStatus).toHaveBeenCalledTimes(1); + }); }); describe('4. resolveLeadDetailText 只增文案', () => { @@ -272,12 +320,58 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', () expect(resolveLeadDetailText('comingSoon')).toBe('即将提供'); expect(resolveLeadDetailText('uploadHint').length).toBeGreaterThan(0); expect(resolveLeadDetailText('save').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('stageTitle').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('historyTitle').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('stageUpdated').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('copiedToast').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('whatsappAction').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('saveContactAction').length).toBeGreaterThan(0); + expect(resolveLeadDetailText('generateAiAction').length).toBeGreaterThan(0); + }); + }); + + describe('5. 销售触达与 AI 赋能函数', () => { + it('5.1 formatWhatsAppUrl 清洗国际号码并生成 wa.me 链接', () => { + const { formatWhatsAppUrl } = loadUseLeadDetail(); + + expect(formatWhatsAppUrl('+971-50-1234567')).toBe('https://wa.me/971501234567'); + expect(formatWhatsAppUrl('0086 138-0000-0000')).toBe('https://wa.me/8613800000000'); + expect(formatWhatsAppUrl('')).toBe(''); + expect(formatWhatsAppUrl(null)).toBe(''); + expect(formatWhatsAppUrl(undefined)).toBe(''); + }); + + it('5.2 generateAiIcebreaker 生成个性化破冰销售话术', () => { + const { generateAiIcebreaker } = loadUseLeadDetail(); + const lead = buildLead({ name: 'Elena Rostova', company: 'Dubai Global Trading' }); + const tracks = [buildTrack({ viewedBlocks: ['case', 'pricing'] })]; + + const scriptZh = generateAiIcebreaker(lead, tracks, 'zh-CN'); + expect(scriptZh).toContain('Elena Rostova'); + expect(scriptZh.length).toBeGreaterThan(20); + + const scriptEn = generateAiIcebreaker(lead, tracks, 'en-US'); + expect(scriptEn).toContain('Elena'); + expect(scriptEn.length).toBeGreaterThan(20); + }); + + it('5.3 appendStructuredNote 结构化拼接备忘与附件链接', () => { + const { appendStructuredNote } = loadUseLeadDetail(); + + const resWithAttach = appendStructuredNote( + 'Initial meeting', + 'Follow-up call agreed', + ['https://cdn.example.com/card.jpg'] + ); + expect(resWithAttach).toContain('Initial meeting'); + expect(resWithAttach).toContain('Follow-up call agreed'); + expect(resWithAttach).toContain('https://cdn.example.com/card.jpg'); }); }); }); describe('OmniCard AI 名片主潜客详情页契约 (leads-detail.uvue)', () => { - it('页面存在,足迹用 list-view,备忘用 textarea,单一主 CTA', () => { + it('页面存在,足迹用 list-view,备忘用 textarea,单一主 CTA,支持 WhatsApp 与 AI 破冰', () => { expect(fs.existsSync(DETAIL_PAGE)).toBe(true); expect(fs.existsSync(DETAIL_UTS)).toBe(true); @@ -294,17 +388,24 @@ describe('OmniCard AI 名片主潜客详情页契约 (leads-detail.uvue)', () => expect(pageSource.includes(' - - - {{ emptyMessage }} - - - - - - - - - - - - {{ state.name }} - {{ state.company }} - - {{ scoreLabel }} {{ state.intentScore }} - {{ statusText }} - + + + + {{ emptyMessage }} + - - {{ timelineLabel }} - - - {{ t('empty') }} - - - {{ formatPlace(item.country, item.city) }} - {{ item.touchType }} - - {{ t('stay') }} {{ formatStayMs(item.stayDurationMs) }} - {{ t('depth') }} {{ formatDepthPct(item.scrollDepth) }} + + + + + + + + + + + + {{ leadInitial }} + + + {{ state.name }} + {{ state.company }} + + + + {{ scoreLabel }} {{ state.intentScore }} + + + {{ statusText }} + - - - - {{ notesLabel }} - {{ state.existingNotes }} -