/** * OmniCard AI (CRMmp) - 用户端 mpuniapp 一键存入手机通讯录 (vCard 动态导出与原生导入) 单元测试 * 严格按照 AGENTS.md 3.3、4.3、4.4 规范与 PRD 3.1 节编写 (TDD Red -> Green) */ import { CardProfile } from '../../src/comm/types/omni'; describe('OmniCard AI 用户端 vCard 导出与通讯录集成 (vcard-export.uts)', () => { let toastCalls: Array<{ title: string; icon?: string }>; let addPhoneContactCalls: Array; let mockAddPhoneContactResult: { success: boolean; errMsg?: string }; beforeEach(() => { toastCalls = []; addPhoneContactCalls = []; mockAddPhoneContactResult = { success: true }; // 全局 uni 运行时 Mock (global as any).uni = { showToast: (options: { title: string; icon?: string }) => { toastCalls.push(options); }, addPhoneContact: (options: any) => { addPhoneContactCalls.push(options); if (mockAddPhoneContactResult.success) { options.success?.({ errMsg: 'addPhoneContact:ok' }); } else { options.fail?.({ errMsg: mockAddPhoneContactResult.errMsg || 'addPhoneContact:fail cancel' }); } options.complete?.(); }, }; }); afterEach(() => { delete (global as any).uni; delete (global as any).window; delete (global as any).Blob; }); const fullCard: CardProfile = { id: 'card_1001', tenantId: 'tenant_001', userId: 'user_001', name: 'Alex Morgan', title: 'Chief Technology Officer', company: 'OmniCard AI Global Inc.', slogan: 'Empowering global trade with AI and NFC technology.', avatarUrl: 'https://cdn.omnicard.ai/avatars/alex.jpg', phone: '+86-13800138000', email: 'alex.morgan@omnicard.ai', whatsapp: '+8613800138000', linkedin: 'https://www.linkedin.com/in/alex-morgan', timezone: 'Asia/Shanghai', status: 'published', themeCode: 'corporate_navy', url: 'https://omnicard.ai/u/alex-morgan', address: '100 Innovation Boulevard', city: 'Shenzhen', country: 'China', }; describe('1. 辅助方法与 RFC 6350 规范支持', () => { it('1.1 splitName 应能智能拆分中英文姓名 (firstName 与 lastName)', () => { // @ts-ignore const { splitName } = require('../../../mpuniapp/common/vcard-export.uts'); // 英文单空格 expect(splitName('Alex Morgan')).toEqual({ firstName: 'Alex', lastName: 'Morgan' }); // 英文多空格 expect(splitName('Dr. Alex J. Morgan')).toEqual({ firstName: 'Dr. Alex J.', lastName: 'Morgan' }); // 中文 2 字 expect(splitName('张三')).toEqual({ firstName: '三', lastName: '张' }); // 中文 3 字 expect(splitName('张小龙')).toEqual({ firstName: '小龙', lastName: '张' }); // 中文常见复姓 4 字 expect(splitName('欧阳六分')).toEqual({ firstName: '六分', lastName: '欧阳' }); expect(splitName('诸葛孔明')).toEqual({ firstName: '孔明', lastName: '诸葛' }); // 单字 expect(splitName('李')).toEqual({ firstName: '李', lastName: '' }); // 空字符 expect(splitName('')).toEqual({ firstName: '名片联系人', lastName: '' }); }); it('1.2 escapeVCardText 应正确转义特殊字符 (反斜杠, 分号, 逗号, 换行符)', () => { // @ts-ignore const { escapeVCardText } = require('../../../mpuniapp/common/vcard-export.uts'); const raw = 'Special\\Name; with, comma\nand newline\r\nand CRLF'; const escaped = escapeVCardText(raw); expect(escaped).toContain('\\\\'); expect(escaped).toContain('\\;'); expect(escaped).toContain('\\,'); expect(escaped).toContain('\\n'); expect(escaped).not.toContain('\r'); }); it('1.3 foldVCardLine 应严格遵守 75 字符行折叠规范', () => { // @ts-ignore const { foldVCardLine } = require('../../../mpuniapp/common/vcard-export.uts'); const shortLine = 'FN:Alex Morgan'; expect(foldVCardLine(shortLine)).toBe(shortLine); const longLine = 'NOTE:This is an extremely long note designed to verify RFC 6350 line folding mechanism with more than seventy-five characters.'; const folded = foldVCardLine(longLine, 75); const lines = folded.split('\r\n'); expect(lines.length).toBeGreaterThan(1); expect(lines[0].length).toBeLessThanOrEqual(75); for (let i = 1; i < lines.length; i++) { expect(lines[i].startsWith(' ')).toBe(true); expect(lines[i].length).toBeLessThanOrEqual(75); } }); it('1.4 buildVCardString 应生成完备合规的 RFC 6350 / RFC 2426 vCard 文本', () => { // @ts-ignore const { buildVCardString } = require('../../../mpuniapp/common/vcard-export.uts'); const vcard = buildVCardString(fullCard); // 基本结构 expect(vcard.startsWith('BEGIN:VCARD\r\n')).toBe(true); expect(vcard.endsWith('END:VCARD\r\n')).toBe(true); expect(vcard).toContain('VERSION:3.0\r\n'); // 核心字段 expect(vcard).toContain('FN:Alex Morgan\r\n'); expect(vcard).toContain('N:Morgan;Alex;;;\r\n'); expect(vcard).toContain('ORG:OmniCard AI Global Inc.\r\n'); expect(vcard).toContain('TITLE:Chief Technology Officer\r\n'); expect(vcard).toContain('TEL;TYPE=CELL:+86-13800138000\r\n'); expect(vcard).toContain('EMAIL;TYPE=WORK:alex.morgan@omnicard.ai\r\n'); expect(vcard).toContain('URL:https://omnicard.ai/u/alex-morgan\r\n'); expect(vcard).toContain('NOTE:Empowering global trade with AI and NFC technology.\r\n'); expect(vcard).toContain('X-SOCIALPROFILE;TYPE=whatsapp:+8613800138000\r\n'); expect(vcard).toContain('X-SOCIALPROFILE;TYPE=linkedin:https://www.linkedin.com/in/alex-morgan\r\n'); expect(vcard).toContain('PHOTO;VALUE=URI:https://cdn.omnicard.ai/avatars/alex.jpg\r\n'); expect(vcard).toContain('ADR;TYPE=WORK:;;100 Innovation Boulevard;Shenzhen;;;China\r\n'); }); it('1.5 buildVCardString 对极简名片与空字段应优雅防御', () => { // @ts-ignore const { buildVCardString } = require('../../../mpuniapp/common/vcard-export.uts'); const minimalCard: CardProfile = { name: 'Simple User', } as any; const vcard = buildVCardString(minimalCard); expect(vcard).toContain('BEGIN:VCARD'); expect(vcard).toContain('FN:Simple User'); expect(vcard).not.toContain('TEL;'); expect(vcard).not.toContain('EMAIL;'); expect(vcard).not.toContain('ORG;'); expect(vcard).toContain('END:VCARD'); }); }); describe('2. App 原生端通讯录写入能力 (uni.addPhoneContact)', () => { it('2.1 saveToContacts 应正确映射字段并调用 uni.addPhoneContact', async () => { // @ts-ignore const { saveToContacts } = require('../../../mpuniapp/common/vcard-export.uts'); const result = await saveToContacts(fullCard); expect(result).toBe(true); expect(addPhoneContactCalls.length).toBe(1); const params = addPhoneContactCalls[0]; expect(params.firstName).toBe('Alex'); expect(params.lastName).toBe('Morgan'); expect(params.organization).toBe('OmniCard AI Global Inc.'); expect(params.title).toBe('Chief Technology Officer'); expect(params.mobilePhoneNumber).toBe('+86-13800138000'); expect(params.email).toBe('alex.morgan@omnicard.ai'); expect(params.remark).toBe('Empowering global trade with AI and NFC technology.'); expect(params.url).toBe('https://omnicard.ai/u/alex-morgan'); expect(params.workAddressStreet).toBe('100 Innovation Boulevard'); expect(params.workAddressCity).toBe('Shenzhen'); expect(params.workAddressCountry).toBe('China'); // 验证成功 Toast expect(toastCalls.length).toBe(1); expect(toastCalls[0].title).toBe('已存入通讯录'); }); it('2.2 当 uni.addPhoneContact 失败或取消时友好兜底且不崩溃', async () => { // @ts-ignore const { saveToContacts } = require('../../../mpuniapp/common/vcard-export.uts'); mockAddPhoneContactResult = { success: false, errMsg: 'addPhoneContact:fail cancel', }; const result = await saveToContacts(fullCard); expect(result).toBe(false); expect(addPhoneContactCalls.length).toBe(1); expect(toastCalls.length).toBe(1); expect(toastCalls[0].title).toBe('未存入通讯录'); }); }); describe('3. H5/Web 端降级与 vCard (.vcf) 自动下载', () => { it('3.1 在 Web/H5 环境无原生 API 时,自动生成 .vcf Blob 并触发下载', async () => { // 移除 uni.addPhoneContact 模拟纯 H5 环境 delete (global as any).uni.addPhoneContact; let createdBlob: any = null; let clicked = false; let downloadAttr = ''; let hrefAttr = ''; let revokedUrl = ''; // 模拟全局 Blob 与 window.document (global as any).Blob = class MockBlob { content: any[]; options: any; constructor(content: any[], options: any) { this.content = content; this.options = options; createdBlob = this; } }; (global as any).window = { URL: { createObjectURL: jest.fn((blob) => 'blob:mock-url-123'), revokeObjectURL: jest.fn((url) => { revokedUrl = url; }), }, document: { createElement: jest.fn((tag) => { if (tag === 'a') { return { style: {}, setAttribute: (name: string, val: string) => { if (name === 'download') downloadAttr = val; }, set href(val: string) { hrefAttr = val; }, get href() { return hrefAttr; }, click: () => { clicked = true; }, }; } return {}; }), body: { appendChild: jest.fn(), removeChild: jest.fn(), }, }, }; // @ts-ignore const { saveToContacts } = require('../../../mpuniapp/common/vcard-export.uts'); const result = await saveToContacts(fullCard); expect(result).toBe(true); expect(createdBlob).not.toBeNull(); expect(createdBlob.options.type).toContain('text/vcard'); expect(downloadAttr).toBe('Alex Morgan.vcf'); expect(hrefAttr).toBe('blob:mock-url-123'); expect(clicked).toBe(true); // 成功 Toast 提示 expect(toastCalls.length).toBe(1); expect(toastCalls[0].title).toBe('已存入通讯录'); }); }); });