feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
(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;
|
||||
});
|
||||
|
||||
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>;
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户