/** * 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; }; 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; 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 { return { 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 }).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 }).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 => { 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 => { 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('