- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
238 行
7.9 KiB
TypeScript
238 行
7.9 KiB
TypeScript
/**
|
|
* OmniCard AI (CRMmp) - 名片主参展分享码海报测试套件 (TDD Red -> Green)
|
|
*
|
|
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/card/qrcode 设计编写
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const QRCODE_PAGE = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/owner/card/qrcode.uvue'
|
|
);
|
|
const QRCODE_UTS = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/owner/card/useCardQrcode.uts'
|
|
);
|
|
|
|
type QrcodeQuery = {
|
|
slug?: string;
|
|
id?: string;
|
|
};
|
|
|
|
type CardQrcodePoster = {
|
|
svg: string;
|
|
url: string;
|
|
};
|
|
|
|
type LoadPosterApi = {
|
|
fetchCardQrcodeSvg: (slugOrId: string) => Promise<CardQrcodePoster>;
|
|
};
|
|
|
|
type LoadPosterResult = {
|
|
svg: string;
|
|
url: string;
|
|
empty: boolean;
|
|
errorMessage: string;
|
|
};
|
|
|
|
type UseCardQrcodeModule = {
|
|
resolveSlugOrId: (query: QrcodeQuery | null | undefined) => string;
|
|
loadPoster: (
|
|
slugOrId: string | null | undefined,
|
|
api?: LoadPosterApi
|
|
) => Promise<LoadPosterResult>;
|
|
copyPosterUrl: (url: string | null | undefined) => boolean;
|
|
};
|
|
|
|
const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
|
|
|
|
function loadUseCardQrcode(): UseCardQrcodeModule {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require(QRCODE_UTS) as UseCardQrcodeModule;
|
|
}
|
|
|
|
function buildPoster(overrides?: Partial<CardQrcodePoster>): CardQrcodePoster {
|
|
return {
|
|
svg: '<svg xmlns="http://www.w3.org/2000/svg"></svg>',
|
|
url: 'https://omnicard.ai/c/alex-vance',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('OmniCard AI 名片主参展分享码海报 (useCardQrcode.uts)', () => {
|
|
let clipboardText: string;
|
|
let toastCalls: Array<{ title: string; icon?: string }>;
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
clipboardText = '';
|
|
toastCalls = [];
|
|
(global as { uni?: Record<string, unknown> }).uni = {
|
|
getLocale: () => 'zh-CN',
|
|
showToast: (options: { title: string; icon?: string }) => {
|
|
toastCalls.push(options);
|
|
},
|
|
setClipboardData: (options: { data: string; success?: () => void }) => {
|
|
clipboardText = options.data;
|
|
options.success?.();
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete (global as { uni?: Record<string, unknown> }).uni;
|
|
});
|
|
|
|
describe('1. resolveSlugOrId 优先 slug,否则 id', () => {
|
|
it('1.1 同时有 slug 与 id 时只取 slug', () => {
|
|
const { resolveSlugOrId } = loadUseCardQrcode();
|
|
|
|
expect(
|
|
resolveSlugOrId({ slug: 'alex-vance', id: '101' })
|
|
).toBe('alex-vance');
|
|
});
|
|
|
|
it('1.2 slug 为空时回退 id,并去掉首尾空白', () => {
|
|
const { resolveSlugOrId } = loadUseCardQrcode();
|
|
|
|
expect(resolveSlugOrId({ slug: ' ', id: ' 101 ' })).toBe('101');
|
|
expect(resolveSlugOrId({ id: 'card-88' })).toBe('card-88');
|
|
expect(resolveSlugOrId({ slug: ' gitex-2026 ' })).toBe('gitex-2026');
|
|
});
|
|
|
|
it('1.3 缺参或全空返回空串', () => {
|
|
const { resolveSlugOrId } = loadUseCardQrcode();
|
|
|
|
expect(resolveSlugOrId({})).toBe('');
|
|
expect(resolveSlugOrId({ slug: '', id: '' })).toBe('');
|
|
expect(resolveSlugOrId(null)).toBe('');
|
|
expect(resolveSlugOrId(undefined)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('2. loadPoster 调用 svg API', () => {
|
|
it('2.1 把 slugOrId 交给 fetchCardQrcodeSvg,并回填 svg / url', async () => {
|
|
const { loadPoster } = loadUseCardQrcode();
|
|
const poster = buildPoster();
|
|
const fetchCardQrcodeSvg = jest.fn(
|
|
async (slugOrId: string): Promise<CardQrcodePoster> => {
|
|
expect(slugOrId).toBe('alex-vance');
|
|
return poster;
|
|
}
|
|
);
|
|
|
|
const result = await loadPoster('alex-vance', { fetchCardQrcodeSvg });
|
|
|
|
expect(fetchCardQrcodeSvg).toHaveBeenCalledTimes(1);
|
|
expect(fetchCardQrcodeSvg).toHaveBeenCalledWith('alex-vance');
|
|
expect(result.svg).toBe(poster.svg);
|
|
expect(result.url).toBe(poster.url);
|
|
expect(result.empty).toBe(false);
|
|
expect(result.errorMessage).toBe('');
|
|
});
|
|
|
|
it('2.2 空 slugOrId 不请求接口,标记空态', async () => {
|
|
const { loadPoster } = loadUseCardQrcode();
|
|
const fetchCardQrcodeSvg = jest.fn(async () => buildPoster());
|
|
|
|
const result = await loadPoster(' ', { fetchCardQrcodeSvg });
|
|
|
|
expect(fetchCardQrcodeSvg).not.toHaveBeenCalled();
|
|
expect(result.svg).toBe('');
|
|
expect(result.url).toBe('');
|
|
expect(result.empty).toBe(true);
|
|
expect(result.errorMessage).toBe('');
|
|
});
|
|
|
|
it('2.3 接口失败写入错误,不伪装空态成功', async () => {
|
|
const { loadPoster } = loadUseCardQrcode();
|
|
const fetchCardQrcodeSvg = jest.fn(async (): Promise<CardQrcodePoster> => {
|
|
throw new Error('qrcode svg unavailable');
|
|
});
|
|
|
|
const result = await loadPoster('alex-vance', { fetchCardQrcodeSvg });
|
|
|
|
expect(fetchCardQrcodeSvg).toHaveBeenCalledTimes(1);
|
|
expect(result.svg).toBe('');
|
|
expect(result.url).toBe('');
|
|
expect(result.empty).toBe(false);
|
|
expect(result.errorMessage).toBe('qrcode svg unavailable');
|
|
});
|
|
});
|
|
|
|
describe('3. copyPosterUrl 复制短链', () => {
|
|
it('3.1 调用 uni.setClipboardData 并提示已复制', () => {
|
|
const { copyPosterUrl } = loadUseCardQrcode();
|
|
const ok = copyPosterUrl('https://omnicard.ai/c/alex-vance');
|
|
|
|
expect(ok).toBe(true);
|
|
expect(clipboardText).toBe('https://omnicard.ai/c/alex-vance');
|
|
expect(toastCalls.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('3.2 空 url 不写剪贴板', () => {
|
|
const { copyPosterUrl } = loadUseCardQrcode();
|
|
const ok = copyPosterUrl(' ');
|
|
|
|
expect(ok).toBe(false);
|
|
expect(clipboardText).toBe('');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('OmniCard AI 参展分享码海报页存在 (qrcode.uvue)', () => {
|
|
it('页面文件存在,展示短链与单一复制 CTA', () => {
|
|
expect(fs.existsSync(QRCODE_PAGE)).toBe(true);
|
|
expect(fs.existsSync(QRCODE_UTS)).toBe(true);
|
|
|
|
const pageSource = fs.readFileSync(QRCODE_PAGE, 'utf8');
|
|
const logicSource = fs.readFileSync(QRCODE_UTS, 'utf8');
|
|
const combined = `${pageSource}\n${logicSource}`;
|
|
|
|
expect(pageSource.includes('script setup lang="uts"')).toBe(true);
|
|
expect(pageSource.includes('getOmniText')).toBe(true);
|
|
expect(pageSource.includes('primary-cta-btn')).toBe(true);
|
|
expect(
|
|
combined.includes('fetchCardQrcodeSvg') || combined.includes('loadPoster')
|
|
).toBe(true);
|
|
expect(
|
|
combined.includes('resolveSlugOrId') || pageSource.includes('slug')
|
|
).toBe(true);
|
|
expect(
|
|
combined.includes('setClipboardData') || combined.includes('copyPosterUrl')
|
|
).toBe(true);
|
|
expect(pageSource.includes('state.url') || pageSource.includes('posterUrl')).toBe(
|
|
true
|
|
);
|
|
expect(pageSource.includes('#0B5CFF')).toBe(true);
|
|
expect(pageSource.includes('#F7F8FA')).toBe(true);
|
|
expect(pageSource.includes('#101828')).toBe(true);
|
|
expect(pageSource.includes('#667085')).toBe(true);
|
|
expect(pageSource.includes('#E3E7ED')).toBe(true);
|
|
expect(pageSource.includes('12px')).toBe(true);
|
|
expect(pageSource.includes('16px')).toBe(true);
|
|
});
|
|
|
|
it('无 emoji、无 any,模板无硬编码中文主文案,仅一个主 CTA', () => {
|
|
const pageSource = fs.readFileSync(QRCODE_PAGE, 'utf8');
|
|
const logicSource = fs.readFileSync(QRCODE_UTS, 'utf8');
|
|
const testSource = fs.readFileSync(__filename, 'utf8');
|
|
const template = pageSource.split('<script')[0];
|
|
|
|
expect(EMOJI_RE.test(pageSource)).toBe(false);
|
|
expect(EMOJI_RE.test(logicSource)).toBe(false);
|
|
expect(EMOJI_RE.test(testSource)).toBe(false);
|
|
expect(pageSource.includes(': any') || pageSource.includes(' as any')).toBe(
|
|
false
|
|
);
|
|
expect(logicSource.includes(': any') || logicSource.includes(' as any')).toBe(
|
|
false
|
|
);
|
|
expect(/[\u4e00-\u9fff]/.test(template)).toBe(false);
|
|
expect((pageSource.match(/primary-cta-btn/g) || []).length).toBeGreaterThan(0);
|
|
expect((template.match(/<button/g) || []).length).toBe(1);
|
|
});
|
|
});
|