283 行
10 KiB
TypeScript
283 行
10 KiB
TypeScript
/**
|
|
* 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<any>;
|
|
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('已存入通讯录');
|
|
});
|
|
});
|
|
});
|