/** * OmniCard AI (CRMmp) - mpuniapp 无摩擦互换名片留资测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD pages/portal/exchange 设计编写 */ type UniToastOption = { title: string; icon?: string; }; type SubmitLeadRequestData = { cardId?: string | number; name?: string; company?: string; phone?: string; email?: string; notes?: string; source?: string; visitorId?: string; }; type UniRequestSuccessRes = { statusCode: number; data: unknown; header: Record; }; type UniRequestOption = { url: string; method?: string; data?: SubmitLeadRequestData; success?: (res: UniRequestSuccessRes) => void; fail?: (err: { errMsg: string }) => void; complete?: () => void; }; describe('OmniCard AI 用户端无摩擦互换名片业务逻辑测试 (useExchange.uts)', () => { let storageMap: Map; let toastCalls: UniToastOption[]; let lastRequestOptions: UniRequestOption | null; let requestCallCount: number; let mockRequestResponse: { statusCode?: number; data?: { code?: number; message?: string; data?: unknown; }; failError?: { errMsg: string }; }; beforeEach(() => { storageMap = new Map(); toastCalls = []; lastRequestOptions = null; requestCallCount = 0; mockRequestResponse = { statusCode: 200, data: { code: 1000, message: 'success', data: { id: 'lead_8801', cardId: 'card-101', name: 'Tariq Mansour', status: 'new', intentScore: 20, persisted: true, }, }, }; (global as { uni?: Record }).uni = { getStorageSync: (key: string) => storageMap.get(key) || '', setStorageSync: (key: string, val: string) => storageMap.set(key, val), removeStorageSync: (key: string) => storageMap.delete(key), showToast: (options: UniToastOption) => { toastCalls.push(options); }, reLaunch: jest.fn(), request: (options: UniRequestOption) => { requestCallCount += 1; 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: jest.fn(), }; }, }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); const SUCCESS_TOAST_RE = /已提交|已完成|已互换|已保存|交换成功|提交成功|Card Exchanged/i; function assertNoSuccessToast(): void { for (const toast of toastCalls) { expect(SUCCESS_TOAST_RE.test(toast.title)).toBe(false); } } function loadUseExchange() { // eslint-disable-next-line @typescript-eslint/no-require-imports return require('../../../mpuniapp/pages/portal/useExchange.uts') as { createExchangeState: () => { form: { name: string; company: string; phone: string; email: string; notes: string; }; cardId: string; slug: string; submitting: boolean; canSubmit: boolean; errorMessage: string; }; applyExchangeOptions: ( state: ReturnType extends never ? never : unknown, options: { cardId?: string; slug?: string; id?: string } | null ) => boolean; submitExchange: (state: { form: { name: string; company: string; phone: string; email: string; notes: string; }; cardId: string; slug: string; submitting: boolean; canSubmit: boolean; errorMessage: string; }) => Promise; isExchangeSubmitSuccess: (lead: unknown) => boolean; }; } describe('1. 表单校验不触发 submitLead,且不得报成功', () => { it('1.1 缺少姓名时不调用 submitLead,不弹出已互换/已保存', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; state.form.phone = '+971501234567'; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(0); expect(lastRequestOptions).toBeNull(); expect(toastCalls.length).toBeGreaterThan(0); expect(toastCalls[0].title).toContain('姓名'); assertNoSuccessToast(); }); it('1.2 缺少 WhatsApp 与邮箱时不调用 submitLead,不报成功', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; state.form.name = 'Tariq Mansour'; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(0); expect(lastRequestOptions).toBeNull(); expect(toastCalls.length).toBeGreaterThan(0); assertNoSuccessToast(); }); it('1.3 姓名与联系方式全空时提交不得报成功', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(0); expect(state.form.name).toBe(''); assertNoSuccessToast(); }); }); describe('2. 合法数据调用 submitLead 并携带 cardId', () => { it('2.1 姓名 + WhatsApp 会提交并带上 cardId,仅明确成功才提示', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; state.form.name = 'Tariq Mansour'; state.form.company = 'Dubai Energy Holding'; state.form.phone = '+971501234567'; state.form.notes = 'Interested in enterprise solution'; const result = await submitExchange(state); expect(result).not.toBeNull(); expect(requestCallCount).toBe(1); expect(lastRequestOptions).not.toBeNull(); expect(lastRequestOptions?.method).toBe('POST'); expect(lastRequestOptions?.url).toContain('/open/crm/lead/exchange'); expect(lastRequestOptions?.data?.cardId).toBe('card-101'); expect(lastRequestOptions?.data?.name).toBe('Tariq Mansour'); expect(lastRequestOptions?.data?.phone).toBe('+971501234567'); expect(state.form.name).toBe(''); expect(state.form.phone).toBe(''); expect(state.form.notes).toBe(''); expect(toastCalls.some(t => SUCCESS_TOAST_RE.test(t.title))).toBe(true); }); it('2.2 姓名 + 邮箱同样会调用 submitLead', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-202' }); state.form.name = 'Alex Vance'; state.form.email = 'alex.vance@nexustech.global'; await submitExchange(state); expect(requestCallCount).toBe(1); expect(lastRequestOptions?.data?.cardId).toBe('card-202'); expect(lastRequestOptions?.data?.email).toBe('alex.vance@nexustech.global'); }); }); describe('4. 接口失败或未落库不得报互换完成', () => { it('4.1 接口失败时返回 null,不弹出已互换/已保存', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); mockRequestResponse = { statusCode: 200, data: { code: 1001, message: '交换名片失败' }, }; const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; state.form.name = 'Tariq Mansour'; state.form.phone = '+971501234567'; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(1); expect(state.form.name).toBe('Tariq Mansour'); assertNoSuccessToast(); }); it('4.2 persisted 不为 true 时不得报成功、不得清空表单', async () => { const { createExchangeState, applyExchangeOptions, submitExchange, isExchangeSubmitSuccess, } = loadUseExchange(); mockRequestResponse.data = { code: 1000, message: 'success', data: { id: 'lead_mem_1', cardId: 'card-101', name: 'Tariq Mansour', status: 'new', intentScore: 20, persisted: false, }, }; expect(isExchangeSubmitSuccess({ id: 'x', persisted: false })).toBe(false); expect(isExchangeSubmitSuccess({ id: 'x', persisted: true })).toBe(true); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); toastCalls = []; state.form.name = 'Tariq Mansour'; state.form.phone = '+971501234567'; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(1); expect(state.form.name).toBe('Tariq Mansour'); expect(state.errorMessage.length).toBeGreaterThan(0); assertNoSuccessToast(); }); }); describe('5. 页面仅在明确成功时展示完成态', () => { it('5.1 exchange.uvue 用 isExchangeSubmitSuccess 判定再置 submitSuccess', () => { const fs = require('fs') as typeof import('fs'); const path = require('path') as typeof import('path'); const pagePath = path.resolve( __dirname, '../../../mpuniapp/pages/portal/exchange.uvue' ); const pageSource = fs.readFileSync(pagePath, 'utf8'); expect(pageSource.includes('isExchangeSubmitSuccess')).toBe(true); expect(pageSource.includes('submitSuccess.value = true')).toBe(true); expect(pageSource.includes('successTitle')).toBe(true); const submitFn = pageSource.indexOf('async function handleSubmit'); expect(submitFn).toBeGreaterThan(-1); const submitBody = pageSource.slice(submitFn, submitFn + 600); expect(submitBody.includes('isExchangeSubmitSuccess')).toBe(true); expect(submitBody.includes('submitSuccess.value = true')).toBe(true); }); }); describe('3. onLoad 名片标识', () => { it('3.1 缺少 cardId 与 slug 时 toast 并禁止提交', () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); const ok = applyExchangeOptions(state, {}); expect(ok).toBe(false); expect(state.canSubmit).toBe(false); expect(toastCalls.length).toBeGreaterThan(0); expect(toastCalls[0].title).toContain('名片'); }); it('3.2 无名片标识时即使表单完整也不调用 submitLead', async () => { const { createExchangeState, submitExchange } = loadUseExchange(); const state = createExchangeState(); state.form.name = 'Tariq Mansour'; state.form.phone = '+971501234567'; const result = await submitExchange(state); expect(result).toBeNull(); expect(requestCallCount).toBe(0); }); it('3.3 仅有 slug 时允许提交,并以 slug 作为 cardId', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); const ok = applyExchangeOptions(state, { slug: 'alex-vance' }); expect(ok).toBe(true); expect(state.canSubmit).toBe(true); state.form.name = 'Alex Vance'; state.form.email = 'alex.vance@nexustech.global'; await submitExchange(state); expect(requestCallCount).toBe(1); expect(lastRequestOptions?.data?.cardId).toBe('alex-vance'); }); }); });