/** * 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, }, }, }; (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; }); 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; }; } describe('1. 表单校验不触发 submitLead', () => { it('1.1 缺少姓名时不调用 submitLead', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); 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('姓名'); }); it('1.2 缺少 WhatsApp 与邮箱时不调用 submitLead', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); 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); }); }); describe('2. 合法数据调用 submitLead 并携带 cardId', () => { it('2.1 姓名 + WhatsApp 会提交并带上 cardId', async () => { const { createExchangeState, applyExchangeOptions, submitExchange } = loadUseExchange(); const state = createExchangeState(); applyExchangeOptions(state, { cardId: 'card-101' }); 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(''); }); 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('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'); }); }); });