文件
OmniCard/backend/test/mpuniapp/owner-card-home.test.ts
wikiboss eec259a3a7 feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换
- 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片
- 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转
- 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出
- 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志
- 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票
- 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏
- 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
2026-09-20 23:47:47 +08:00

277 行
8.5 KiB
TypeScript

/**
* 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<T> = {
list: T[];
pagination: {
page: number;
size: number;
total: number;
};
};
type LoadCardsQuery = {
page: number;
size: number;
keyWord?: string;
};
type LoadCardsApi = {
fetchOwnerCardPage: (
query: OwnerPageQuery
) => Promise<OwnerPageData<CardProfile>>;
};
type LoadCardsResult = {
list: CardProfile[];
pagination: {
page: number;
size: number;
total: number;
};
empty: boolean;
errorMessage: string;
};
type UseCardHomeModule = {
loadCards: (
query: LoadCardsQuery,
api?: LoadCardsApi
) => Promise<LoadCardsResult>;
buildEditUrl: (id: string | number) => string;
buildQrcodeUrl: (card: CardProfile) => string;
};
const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
function buildCard(overrides?: Partial<CardProfile>): 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<CardProfile> {
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<string, unknown> }).uni = {
getLocale: () => 'zh-CN',
showToast: jest.fn(),
navigateTo: jest.fn(),
};
});
afterEach(() => {
delete (global as { uni?: Record<string, unknown> }).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<OwnerPageData<CardProfile>> => {
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<OwnerPageData<CardProfile>> => 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<OwnerPageData<CardProfile>> => {
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<OwnerPageData<CardProfile>> => {
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('<list-view')).toBe(true);
expect(pageSource.includes('<list-item')).toBe(true);
expect(pageSource.includes('v-for')).toBe(true);
expect(
combined.includes('fetchOwnerCardPage') || combined.includes('loadCards')
).toBe(true);
expect(pageSource.includes('script setup lang="uts"')).toBe(true);
expect(pageSource.includes('getOmniText')).toBe(true);
expect(pageSource.includes('item.name')).toBe(true);
expect(pageSource.includes('item.title')).toBe(true);
expect(pageSource.includes('item.company')).toBe(true);
expect(pageSource.includes('item.status') || pageSource.includes('statusLabel')).toBe(
true
);
expect(combined.includes('/pages/owner/card/edit?id=')).toBe(true);
expect(
combined.includes('/pages/owner/card/qrcode?id=') ||
combined.includes('/pages/owner/card/qrcode?slug=')
).toBe(true);
expect(pageSource.includes('navigateTo') || logicSource.includes('navigateTo')).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('primary-cta-btn')).toBe(true);
});
it('无 emoji、无 any,模板无硬编码中文主文案,长列表不用 view v-for', () => {
const pageSource = fs.readFileSync(CARD_PAGE, 'utf8');
const logicSource = fs.readFileSync(CARD_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(/<view[^>]*v-for/.test(template)).toBe(false);
expect(template.includes('<list-item')).toBe(true);
});
});