/** * OmniCard AI (CRMmp) - 名片主极速编辑测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/card/edit 设计编写 */ import * as fs from 'fs'; import * as path from 'path'; const EDIT_PAGE = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/edit.uvue' ); const EDIT_UTS = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/useCardEdit.uts' ); type CardProfile = { id?: string | number; name: string; title?: string; company?: string; slogan?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; }; type CardEditForm = { id?: string | number | null; name?: string; title?: string; company?: string; slogan?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; }; type CardUpdatePayload = { id: string | number; name?: string; title?: string; company?: string; slogan?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; }; type LoadCardApi = { fetchOwnerCardInfo?: (id: string | number) => Promise; }; type LoadCardResult = { card: CardProfile | null; empty: boolean; errorMessage: string; }; type SaveCardApi = { updateOwnerCard?: ( payload: CardProfile & { id: string | number } ) => Promise; }; type SaveCardResult = { ok: boolean; card: CardProfile | null; errorMessage: string; }; type UseCardEditModule = { parseCardId: (raw: string | number | null | undefined) => number | null; buildUpdatePayload: (form: CardEditForm) => CardUpdatePayload | null; loadCard: ( id: string | number | null | undefined, api?: LoadCardApi ) => Promise; saveCard: (form: CardEditForm, api?: SaveCardApi) => Promise; }; const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/; function buildCard(overrides?: Partial): CardProfile { return { id: 8, name: 'Alex Vance', title: 'Partner', company: 'Nexus Tech', slogan: 'Build trust first', phone: '+971500000008', email: 'alex@nexus.example', whatsapp: '+971500000008', linkedin: 'https://linkedin.com/in/alexvance', ...overrides, }; } function loadUseCardEdit(): UseCardEditModule { // eslint-disable-next-line @typescript-eslint/no-require-imports return require(EDIT_UTS) as UseCardEditModule; } describe('OmniCard AI 名片主极速编辑业务逻辑 (useCardEdit.uts)', () => { beforeEach(() => { jest.resetModules(); (global as { uni?: Record }).uni = { getLocale: () => 'zh-CN', showToast: jest.fn(), navigateBack: jest.fn(), navigateTo: jest.fn(), }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); describe('1. parseCardId', () => { it('1.1 正整数与数字字符串解析为 id,非法值丢弃', () => { const { parseCardId } = loadUseCardEdit(); expect(parseCardId(8)).toBe(8); expect(parseCardId('8')).toBe(8); expect(parseCardId(' 8 ')).toBe(8); expect(parseCardId('')).toBeNull(); expect(parseCardId('0')).toBeNull(); expect(parseCardId(-3)).toBeNull(); expect(parseCardId('abc')).toBeNull(); expect(parseCardId(null)).toBeNull(); expect(parseCardId(undefined)).toBeNull(); }); }); describe('2. loadCard 调 fetchOwnerCardInfo', () => { it('2.1 注入后按解析后的 id 拉取名片', async () => { const { loadCard } = loadUseCardEdit(); const card = buildCard(); const fetchOwnerCardInfo = jest.fn(async (id: string | number) => { expect(id).toBe(8); return card; }); const result = await loadCard(' 8 ', { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).toHaveBeenCalledTimes(1); expect(result.card).not.toBeNull(); expect(result.card?.name).toBe('Alex Vance'); expect(result.card?.company).toBe('Nexus Tech'); expect(result.empty).toBe(false); expect(result.errorMessage).toBe(''); }); it('2.2 缺 id 不请求接口,标记空态', async () => { const { loadCard } = loadUseCardEdit(); const fetchOwnerCardInfo = jest.fn(async () => buildCard()); const result = await loadCard('', { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).not.toHaveBeenCalled(); expect(result.card).toBeNull(); expect(result.empty).toBe(true); expect(result.errorMessage).toBe(''); }); it('2.3 接口失败写入错误,不伪装空态成功', async () => { const { loadCard } = loadUseCardEdit(); const fetchOwnerCardInfo = jest.fn(async () => { throw new Error('card info unavailable'); }); const result = await loadCard(8, { fetchOwnerCardInfo }); expect(fetchOwnerCardInfo).toHaveBeenCalledTimes(1); expect(result.card).toBeNull(); expect(result.empty).toBe(true); expect(result.errorMessage).toBe('card info unavailable'); }); }); describe('3. saveCard 调 updateOwnerCard', () => { it('3.1 buildUpdatePayload 只带已填字段与 id', () => { const { buildUpdatePayload } = loadUseCardEdit(); const payload = buildUpdatePayload({ id: '8', name: 'Alex Vance', title: ' ', company: 'Nexus Tech', phone: '', email: undefined, whatsapp: '', linkedin: 'https://linkedin.com/in/alexvance', slogan: '', }); expect(payload).not.toBeNull(); expect(payload?.id).toBe(8); expect(payload?.name).toBe('Alex Vance'); expect(payload?.company).toBe('Nexus Tech'); expect(payload?.linkedin).toBe('https://linkedin.com/in/alexvance'); expect(payload?.title).toBeUndefined(); expect(payload?.phone).toBeUndefined(); expect(payload?.email).toBeUndefined(); expect(payload?.whatsapp).toBeUndefined(); expect(payload?.slogan).toBeUndefined(); expect(Object.keys(payload as CardUpdatePayload).sort()).toEqual([ 'company', 'id', 'linkedin', 'name', ]); }); it('3.2 saveCard 把精简 payload 交给 updateOwnerCard', async () => { const { saveCard } = loadUseCardEdit(); const saved = buildCard({ title: undefined, phone: undefined }); const updateOwnerCard = jest.fn( async (payload: CardProfile & { id: string | number }) => { expect(payload.id).toBe(8); expect(payload.name).toBe('Alex Vance'); expect(payload.company).toBe('Nexus Tech'); expect(payload.title).toBeUndefined(); expect(payload.phone).toBeUndefined(); return saved; } ); const result = await saveCard( { id: 8, name: 'Alex Vance', title: '', company: 'Nexus Tech', phone: ' ', }, { updateOwnerCard } ); expect(updateOwnerCard).toHaveBeenCalledTimes(1); expect(result.ok).toBe(true); expect(result.card?.name).toBe('Alex Vance'); expect(result.errorMessage).toBe(''); }); it('3.3 缺 id 不请求 updateOwnerCard', async () => { const { saveCard } = loadUseCardEdit(); const updateOwnerCard = jest.fn(async () => buildCard()); const result = await saveCard( { id: '', name: 'Alex Vance', company: 'Nexus Tech' }, { updateOwnerCard } ); expect(updateOwnerCard).not.toHaveBeenCalled(); expect(result.ok).toBe(false); expect(result.card).toBeNull(); }); }); }); describe('OmniCard AI 名片主极速编辑页契约 (edit.uvue)', () => { it('页面存在且引 updateOwnerCard 或 saveCard', () => { expect(fs.existsSync(EDIT_PAGE)).toBe(true); expect(fs.existsSync(EDIT_UTS)).toBe(true); const pageSource = fs.readFileSync(EDIT_PAGE, 'utf8'); const logicSource = fs.readFileSync(EDIT_UTS, 'utf8'); const combined = `${pageSource}\n${logicSource}`; expect(pageSource.includes('script setup lang="uts"')).toBe(true); expect( combined.includes('updateOwnerCard') || combined.includes('saveCard') ).toBe(true); expect(combined.includes('fetchOwnerCardInfo') || combined.includes('loadCard')).toBe( true ); expect(combined.includes('buildUpdatePayload')).toBe(true); expect(pageSource.includes('v-model="state.name"')).toBe(true); expect(pageSource.includes('v-model="state.title"')).toBe(true); expect(pageSource.includes('v-model="state.company"')).toBe(true); expect(pageSource.includes('v-model="state.phone"')).toBe(true); expect(pageSource.includes('v-model="state.email"')).toBe(true); expect(pageSource.includes('v-model="state.whatsapp"')).toBe(true); expect(pageSource.includes('v-model="state.linkedin"')).toBe(true); expect(pageSource.includes('primary-cta-btn')).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); expect(pageSource.toLowerCase().includes('bento')).toBe(false); expect(pageSource.includes('draggable') || pageSource.includes('@drag')).toBe( false ); }); it('无 emoji、无 any,模板无硬编码中文主文案', () => { const pageSource = fs.readFileSync(EDIT_PAGE, 'utf8'); const logicSource = fs.readFileSync(EDIT_UTS, 'utf8'); const testSource = fs.readFileSync(__filename, 'utf8'); const template = pageSource.split('