补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
387 行
12 KiB
TypeScript
387 行
12 KiB
TypeScript
/**
|
|
* 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<string, string>;
|
|
};
|
|
|
|
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<string, string>;
|
|
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<string, unknown> }).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<string, unknown> }).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<typeof loadUseExchange> 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<unknown>;
|
|
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');
|
|
});
|
|
});
|
|
});
|