/** * OmniCard AI (CRMmp) - 名片主移动名片中心测试套件 (TDD Red -> Green) * * 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/card 设计编写 */ import * as fs from 'fs'; import * as path from 'path'; const CARD_PAGE = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/index.uvue' ); const CARD_UTS = path.resolve( __dirname, '../../../mpuniapp/pages/owner/card/useCardHome.uts' ); type CardStatus = 'draft' | 'published' | 'archived'; type CardProfile = { id?: string | number; name: string; title?: string; company?: string; status?: CardStatus; slug?: string; }; type OwnerPageQuery = { page: number; size: number; keyWord?: string; }; type OwnerPageData = { list: T[]; pagination: { page: number; size: number; total: number; }; }; type LoadCardsQuery = { page: number; size: number; keyWord?: string; }; type LoadCardsApi = { fetchOwnerCardPage: ( query: OwnerPageQuery ) => Promise>; }; type LoadCardsResult = { list: CardProfile[]; pagination: { page: number; size: number; total: number; }; empty: boolean; errorMessage: string; }; type UseCardHomeModule = { loadCards: ( query: LoadCardsQuery, api?: LoadCardsApi ) => Promise; buildEditUrl: (id: string | number) => string; buildQrcodeUrl: (card: CardProfile) => string; }; 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', status: 'published', slug: 'alex-vance', ...overrides, }; } function emptyPage(page: number, size: number): OwnerPageData { return { list: [], pagination: { page, size, total: 0 }, }; } function loadUseCardHome(): UseCardHomeModule { // eslint-disable-next-line @typescript-eslint/no-require-imports return require(CARD_UTS) as UseCardHomeModule; } describe('OmniCard AI 名片主移动名片中心 (useCardHome.uts)', () => { beforeEach(() => { jest.resetModules(); (global as { uni?: Record }).uni = { getLocale: () => 'zh-CN', showToast: jest.fn(), navigateTo: jest.fn(), }; }); afterEach(() => { delete (global as { uni?: Record }).uni; }); describe('1. loadCards 调用 page API', () => { it('1.1 将 page / size / keyWord 交给 fetchOwnerCardPage', async () => { const { loadCards } = loadUseCardHome(); const card = buildCard({ status: 'published' }); const fetchOwnerCardPage = jest.fn( async (query: OwnerPageQuery): Promise> => { expect(query.page).toBe(2); expect(query.size).toBe(20); expect(query.keyWord).toBe('Vance'); return { list: [card], pagination: { page: 2, size: 20, total: 1 }, }; } ); const result = await loadCards( { page: 2, size: 20, keyWord: 'Vance' }, { fetchOwnerCardPage } ); expect(fetchOwnerCardPage).toHaveBeenCalledTimes(1); expect(result.list).toHaveLength(1); expect(result.list[0].name).toBe('Alex Vance'); expect(result.list[0].title).toBe('Partner'); expect(result.list[0].company).toBe('Nexus Tech'); expect(result.list[0].status).toBe('published'); expect(result.empty).toBe(false); expect(result.errorMessage).toBe(''); expect(result.pagination.total).toBe(1); }); it('1.2 空列表标记空态,不写错误', async () => { const { loadCards } = loadUseCardHome(); const fetchOwnerCardPage = jest.fn( async (): Promise> => emptyPage(1, 10) ); const result = await loadCards( { page: 1, size: 10 }, { fetchOwnerCardPage } ); expect(fetchOwnerCardPage).toHaveBeenCalledTimes(1); expect(result.list).toEqual([]); expect(result.empty).toBe(true); expect(result.errorMessage).toBe(''); }); it('1.3 接口失败写入错误,不伪装空态成功', async () => { const { loadCards } = loadUseCardHome(); const fetchOwnerCardPage = jest.fn( async (): Promise> => { throw new Error('card page unavailable'); } ); const result = await loadCards( { page: 1, size: 10, keyWord: 'Nexus' }, { fetchOwnerCardPage } ); expect(fetchOwnerCardPage).toHaveBeenCalledTimes(1); expect(result.list).toEqual([]); expect(result.empty).toBe(false); expect(result.errorMessage).toBe('card page unavailable'); }); it('1.4 空白 keyWord 不传给 page API', async () => { const { loadCards } = loadUseCardHome(); const fetchOwnerCardPage = jest.fn( async (query: OwnerPageQuery): Promise> => { expect(query.keyWord).toBeUndefined(); return emptyPage(query.page, query.size); } ); await loadCards( { page: 1, size: 10, keyWord: ' ' }, { fetchOwnerCardPage } ); expect(fetchOwnerCardPage).toHaveBeenCalledTimes(1); const sent = fetchOwnerCardPage.mock.calls[0][0] as OwnerPageQuery; expect(sent.page).toBe(1); expect(sent.size).toBe(10); expect(sent.keyWord).toBeUndefined(); }); }); describe('2. 编辑与海报跳转契约', () => { it('2.1 行点击走 edit?id=,海报优先 slug', () => { const { buildEditUrl, buildQrcodeUrl } = loadUseCardHome(); expect(buildEditUrl(8)).toBe('/pages/owner/card/edit?id=8'); expect(buildQrcodeUrl(buildCard({ slug: 'alex-vance' }))).toBe( '/pages/owner/card/qrcode?slug=alex-vance' ); expect(buildQrcodeUrl(buildCard({ slug: '', id: 12 }))).toBe( '/pages/owner/card/qrcode?id=12' ); }); }); }); describe('OmniCard AI 名片主名片中心页契约 (index.uvue)', () => { it('页面含 list-view,并引用 fetchOwnerCardPage 或 loadCards', () => { expect(fs.existsSync(CARD_PAGE)).toBe(true); expect(fs.existsSync(CARD_UTS)).toBe(true); const pageSource = fs.readFileSync(CARD_PAGE, 'utf8'); const logicSource = fs.readFileSync(CARD_UTS, 'utf8'); const combined = `${pageSource}\n${logicSource}`; expect(pageSource.includes(' { const pageSource = fs.readFileSync(CARD_PAGE, 'utf8'); const logicSource = fs.readFileSync(CARD_UTS, 'utf8'); const testSource = fs.readFileSync(__filename, 'utf8'); const template = pageSource.split(']*v-for/.test(template)).toBe(false); expect(template.includes('