417 行
14 KiB
TypeScript
417 行
14 KiB
TypeScript
/**
|
||
* OmniCard AI (CRMmp) - 移动端拍照上传纸质名片 OCR 识别入库测试套件 (Point 25)
|
||
* 严格按照 TDD 模式 (Red -> Green -> Refactor) 与 AGENTS.md 规范编写
|
||
*/
|
||
|
||
type ExchangeForm = {
|
||
name: string;
|
||
company: string;
|
||
phone: string;
|
||
email: string;
|
||
notes: string;
|
||
};
|
||
|
||
type ExtractedContactInfo = {
|
||
name: string;
|
||
company: string;
|
||
phone: string;
|
||
email: string;
|
||
title: string;
|
||
notes: string;
|
||
rawText: string;
|
||
};
|
||
|
||
describe('Point 25: 移动端拍照上传纸质名片 OCR 识别入库 (useOcrExchange.uts)', () => {
|
||
let toastCalls: Array<{ title: string; icon?: string }>;
|
||
|
||
beforeEach(() => {
|
||
toastCalls = [];
|
||
(global as unknown as { uni?: Record<string, unknown> }).uni = {
|
||
showToast: (options: { title: string; icon?: string }) => {
|
||
toastCalls.push(options);
|
||
},
|
||
chooseImage: jest.fn(),
|
||
};
|
||
});
|
||
|
||
afterEach(() => {
|
||
delete (global as unknown as { uni?: Record<string, unknown> }).uni;
|
||
});
|
||
|
||
function loadUseOcrExchange() {
|
||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||
return require('../../../mpuniapp/pages/portal/useOcrExchange.uts') as {
|
||
parseOcrCardText: (rawOcrText: string) => ExtractedContactInfo;
|
||
fillFormFromOcr: (
|
||
form: ExchangeForm,
|
||
extracted: ExtractedContactInfo,
|
||
overwrite?: boolean
|
||
) => ExchangeForm;
|
||
normalizeE164Phone: (rawPhone: string) => string;
|
||
createOcrScanState: () => {
|
||
scanning: boolean;
|
||
rawText: string;
|
||
lastExtracted: ExtractedContactInfo | null;
|
||
errorMessage: string;
|
||
};
|
||
simulateOcrScan: (sampleType?: string) => ExtractedContactInfo;
|
||
handleCardOcrUpload: (
|
||
form: ExchangeForm,
|
||
ocrState: ReturnType<typeof loadUseOcrExchange> extends never ? never : any,
|
||
options?: { mockText?: string }
|
||
) => Promise<boolean>;
|
||
};
|
||
}
|
||
|
||
describe('1. 纯文本识别提取 (Basic Plain Text Extraction)', () => {
|
||
it('1.1 提取带有标准冒号标签的名片文本', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const rawText = `
|
||
姓名: 张伟
|
||
职务: 研发总监
|
||
公司: 深圳超维智能科技有限公司
|
||
电话: +86 138-0013-8000
|
||
邮箱: zhangwei@chaowei-ai.com
|
||
`;
|
||
|
||
const result = parseOcrCardText(rawText);
|
||
|
||
expect(result.name).toBe('张伟');
|
||
expect(result.title).toBe('研发总监');
|
||
expect(result.company).toBe('深圳超维智能科技有限公司');
|
||
expect(result.phone).toBe('+8613800138000');
|
||
expect(result.email).toBe('zhangwei@chaowei-ai.com');
|
||
});
|
||
|
||
it('1.2 提取无标签前缀的英文标准商务名片', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const rawText = `
|
||
OmniCard AI Global Inc.
|
||
Alex Morgan
|
||
Chief Technology Officer
|
||
alex.morgan@omnicard.ai
|
||
+1 555-234-5678
|
||
`;
|
||
|
||
const result = parseOcrCardText(rawText);
|
||
|
||
expect(result.name).toBe('Alex Morgan');
|
||
expect(result.title).toBe('Chief Technology Officer');
|
||
expect(result.company).toBe('OmniCard AI Global Inc.');
|
||
expect(result.email).toBe('alex.morgan@omnicard.ai');
|
||
expect(result.phone).toBe('+15552345678');
|
||
});
|
||
});
|
||
|
||
describe('2. 国际标准 E.164 电话与多格式邮箱提取', () => {
|
||
it('2.1 正确识别与规范化中国、阿联酋、英国、美国等国际 E.164 电话', () => {
|
||
const { normalizeE164Phone, parseOcrCardText } = loadUseOcrExchange();
|
||
|
||
// 单独格式化函数验证
|
||
expect(normalizeE164Phone('+86 138 0013 8000')).toBe('+8613800138000');
|
||
expect(normalizeE164Phone('+971 50 123 4567')).toBe('+971501234567');
|
||
expect(normalizeE164Phone('+1 (555) 234-5678')).toBe('+15552345678');
|
||
expect(normalizeE164Phone('+44 20 7946 0919')).toBe('+442079460919');
|
||
expect(normalizeE164Phone('0086 13800138000')).toBe('+8613800138000');
|
||
expect(normalizeE164Phone('13912345678')).toBe('+8613912345678');
|
||
|
||
// 从 OCR 文本中识别中东阿联酋电话
|
||
const uaeCard = `
|
||
Tariq Mansour
|
||
Dubai Trade Center LLC
|
||
WhatsApp: +971 50 123 4567
|
||
tariq@dubaitrade.ae
|
||
`;
|
||
const uaeResult = parseOcrCardText(uaeCard);
|
||
expect(uaeResult.phone).toBe('+971501234567');
|
||
expect(uaeResult.email).toBe('tariq@dubaitrade.ae');
|
||
});
|
||
|
||
it('2.2 提取多级域名及包含加号/下划线的复杂邮箱地址', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const rawText = `
|
||
Contact person: Sarah Connor
|
||
Organization: Cyberdyne Systems UK Ltd.
|
||
E-mail: sarah.connor+biz_dev@corp.subdomain.co.uk
|
||
Mobile: +44 7700 900123
|
||
`;
|
||
|
||
const result = parseOcrCardText(rawText);
|
||
expect(result.email).toBe('sarah.connor+biz_dev@corp.subdomain.co.uk');
|
||
expect(result.name).toBe('Sarah Connor');
|
||
expect(result.company).toBe('Cyberdyne Systems UK Ltd.');
|
||
expect(result.phone).toBe('+447700900123');
|
||
});
|
||
});
|
||
|
||
describe('3. 复杂多行格式、乱序与噪音干扰提取', () => {
|
||
it('3.1 识别同一行内的姓名与职位组合', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const rawText = `
|
||
杭州智算科技有限公司
|
||
李小峰 / 首席架构师
|
||
Tel: +86-571-88889999
|
||
Email: lxf@zhisuan.cn
|
||
`;
|
||
|
||
const result = parseOcrCardText(rawText);
|
||
expect(result.name).toBe('李小峰');
|
||
expect(result.title).toBe('首席架构师');
|
||
expect(result.company).toBe('杭州智算科技有限公司');
|
||
expect(result.email).toBe('lxf@zhisuan.cn');
|
||
});
|
||
|
||
it('3.2 乱序排版及含有地址、网址、标语等干扰信息时准确定位核心字段', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const messyCard = `
|
||
www.futurecloud.io
|
||
service@futurecloud.io
|
||
地址:北京市海淀区中关村南大街1号科技大厦B座18层
|
||
致力于打造全球顶尖分布式智算基础设施
|
||
+86 186 1111 2222
|
||
王建国
|
||
VP of Operations
|
||
北京未来云智科技有限公司
|
||
`;
|
||
|
||
const result = parseOcrCardText(messyCard);
|
||
expect(result.name).toBe('王建国');
|
||
expect(result.title).toBe('VP of Operations');
|
||
expect(result.company).toBe('北京未来云智科技有限公司');
|
||
expect(result.email).toBe('service@futurecloud.io');
|
||
expect(result.phone).toBe('+8618611112222');
|
||
});
|
||
});
|
||
|
||
describe('4. 空文本与极端异常容错 (Resilience & Edge Cases)', () => {
|
||
it('4.1 传入空文本或空白字符时返回安全的空字段结构且不报错', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
|
||
const emptyResult = parseOcrCardText('');
|
||
expect(emptyResult).toEqual({
|
||
name: '',
|
||
company: '',
|
||
phone: '',
|
||
email: '',
|
||
title: '',
|
||
notes: '',
|
||
rawText: '',
|
||
});
|
||
|
||
const whitespaceResult = parseOcrCardText(' \n\t \r\n ');
|
||
expect(whitespaceResult.name).toBe('');
|
||
expect(whitespaceResult.company).toBe('');
|
||
expect(whitespaceResult.phone).toBe('');
|
||
expect(whitespaceResult.email).toBe('');
|
||
});
|
||
|
||
it('4.2 传入无任何有效联系信息的文本时安全容错', () => {
|
||
const { parseOcrCardText } = loadUseOcrExchange();
|
||
const gibberish = '### !@@$ \n --- \n 12345 \n ?????';
|
||
const result = parseOcrCardText(gibberish);
|
||
|
||
expect(result.name).toBe('');
|
||
expect(result.company).toBe('');
|
||
expect(result.phone).toBe('');
|
||
expect(result.email).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('5. fillFormFromOcr 智能填单与缺失字段保护', () => {
|
||
it('5.1 完整信息直接填充空表单', () => {
|
||
const { fillFormFromOcr } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '',
|
||
company: '',
|
||
phone: '',
|
||
email: '',
|
||
notes: '',
|
||
};
|
||
|
||
const extracted: ExtractedContactInfo = {
|
||
name: '张三',
|
||
company: '极客互联',
|
||
phone: '+8613800138000',
|
||
email: 'zhangsan@geek.com',
|
||
title: '产品总监',
|
||
notes: '',
|
||
rawText: '...',
|
||
};
|
||
|
||
fillFormFromOcr(form, extracted);
|
||
|
||
expect(form.name).toBe('张三');
|
||
expect(form.company).toBe('极客互联');
|
||
expect(form.phone).toBe('+8613800138000');
|
||
expect(form.email).toBe('zhangsan@geek.com');
|
||
// 若原 notes 为空且 title 存在,智能将 title 放入 notes 方便跟进
|
||
expect(form.notes).toBe('产品总监');
|
||
});
|
||
|
||
it('5.2 缺失字段保护:OCR 缺失的字段绝不覆盖已有用户输入', () => {
|
||
const { fillFormFromOcr } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '',
|
||
company: '已有预填公司',
|
||
phone: '',
|
||
email: '',
|
||
notes: '已有重要客户需求备注',
|
||
};
|
||
|
||
// OCR 仅识别到了姓名与手机号
|
||
const extracted: ExtractedContactInfo = {
|
||
name: '李四',
|
||
company: '',
|
||
phone: '+8613900001111',
|
||
email: '',
|
||
title: '',
|
||
notes: '',
|
||
rawText: '...',
|
||
};
|
||
|
||
fillFormFromOcr(form, extracted);
|
||
|
||
expect(form.name).toBe('李四');
|
||
expect(form.phone).toBe('+8613900001111');
|
||
// 原有非空内容受到保护,未被清空
|
||
expect(form.company).toBe('已有预填公司');
|
||
expect(form.notes).toBe('已有重要客户需求备注');
|
||
});
|
||
|
||
it('5.3 非覆盖模式 (overwrite: false) 仅填充空白字段', () => {
|
||
const { fillFormFromOcr } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '手动输入的姓名',
|
||
company: '',
|
||
phone: '+8613777777777',
|
||
email: '',
|
||
notes: '',
|
||
};
|
||
|
||
const extracted: ExtractedContactInfo = {
|
||
name: 'OCR识别出的新姓名',
|
||
company: 'OCR公司',
|
||
phone: '+8613888888888',
|
||
email: 'new@corp.com',
|
||
title: '',
|
||
notes: '',
|
||
rawText: '...',
|
||
};
|
||
|
||
fillFormFromOcr(form, extracted, false);
|
||
|
||
// 手动输入的姓名与电话保持原样
|
||
expect(form.name).toBe('手动输入的姓名');
|
||
expect(form.phone).toBe('+8613777777777');
|
||
// 原为空的字段被填入
|
||
expect(form.company).toBe('OCR公司');
|
||
expect(form.email).toBe('new@corp.com');
|
||
});
|
||
});
|
||
|
||
describe('6. 客户端拍照上传流程辅助与模拟 (handleCardOcrUpload)', () => {
|
||
it('6.1 模拟名片识别成功后自动填充表单并提示用户', async () => {
|
||
const { createOcrScanState, handleCardOcrUpload } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '',
|
||
company: '',
|
||
phone: '',
|
||
email: '',
|
||
notes: '',
|
||
};
|
||
const ocrState = createOcrScanState();
|
||
|
||
const mockText = `
|
||
Name: Tariq Mansour
|
||
Company: Al Futtaim Group LLC
|
||
Phone: +971 50 123 4567
|
||
Email: tariq.mansour@alfuttaim.ae
|
||
`;
|
||
|
||
const success = await handleCardOcrUpload(form, ocrState, { mockText });
|
||
|
||
expect(success).toBe(true);
|
||
expect(form.name).toBe('Tariq Mansour');
|
||
expect(form.company).toBe('Al Futtaim Group LLC');
|
||
expect(form.phone).toBe('+971501234567');
|
||
expect(form.email).toBe('tariq.mansour@alfuttaim.ae');
|
||
expect(ocrState.scanning).toBe(false);
|
||
expect(ocrState.lastExtracted).not.toBeNull();
|
||
expect(toastCalls.length).toBeGreaterThan(0);
|
||
expect(toastCalls[0].title).toContain('识别成功');
|
||
});
|
||
|
||
it('6.2 通过 chooseImage 成功拍照或选图后触发名片自动填充', async () => {
|
||
const { createOcrScanState, handleCardOcrUpload } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '',
|
||
company: '',
|
||
phone: '',
|
||
email: '',
|
||
notes: '',
|
||
};
|
||
const ocrState = createOcrScanState();
|
||
|
||
(global as unknown as { uni: Record<string, unknown> }).uni.chooseImage = (opt: {
|
||
success?: (res: unknown) => void;
|
||
fail?: (err: unknown) => void;
|
||
}) => {
|
||
opt.success?.({ tempFilePaths: ['temp://photo.jpg'] });
|
||
};
|
||
|
||
const success = await handleCardOcrUpload(form, ocrState);
|
||
|
||
expect(success).toBe(true);
|
||
expect(form.name).toBe('张伟');
|
||
expect(form.phone).toBe('+8613800138000');
|
||
expect(form.company).toBe('深圳超维智能科技有限公司');
|
||
expect(ocrState.scanning).toBe(false);
|
||
expect(toastCalls.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('6.3 用户取消拍照选图时优雅处理并不覆盖已有表单', async () => {
|
||
const { createOcrScanState, handleCardOcrUpload } = loadUseOcrExchange();
|
||
const form: ExchangeForm = {
|
||
name: '已有姓名',
|
||
company: '',
|
||
phone: '',
|
||
email: '',
|
||
notes: '',
|
||
};
|
||
const ocrState = createOcrScanState();
|
||
|
||
(global as unknown as { uni: Record<string, unknown> }).uni.chooseImage = (opt: {
|
||
success?: (res: unknown) => void;
|
||
fail?: (err: unknown) => void;
|
||
}) => {
|
||
opt.fail?.(new Error('用户取消操作'));
|
||
};
|
||
|
||
const success = await handleCardOcrUpload(form, ocrState);
|
||
|
||
expect(success).toBe(false);
|
||
expect(form.name).toBe('已有姓名');
|
||
expect(ocrState.scanning).toBe(false);
|
||
expect(ocrState.errorMessage).toContain('用户取消操作');
|
||
});
|
||
|
||
it('6.4 simulateOcrScan 针对国内外商务预设名片准确提取', () => {
|
||
const { simulateOcrScan } = loadUseOcrExchange();
|
||
|
||
const uaeSample = simulateOcrScan('uae');
|
||
expect(uaeSample.name).toBe('Tariq Mansour');
|
||
expect(uaeSample.phone).toBe('+971501234567');
|
||
expect(uaeSample.company).toBe('Dubai Trade Center LLC');
|
||
|
||
const globalSample = simulateOcrScan('global');
|
||
expect(globalSample.name).toBe('Alex Morgan');
|
||
expect(globalSample.phone).toBe('+15552345678');
|
||
expect(globalSample.company).toBe('OmniCard AI Global Inc.');
|
||
|
||
const defaultSample = simulateOcrScan();
|
||
expect(defaultSample.name).toBe('张伟');
|
||
expect(defaultSample.phone).toBe('+8613800138000');
|
||
});
|
||
});
|
||
});
|