- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
376 行
12 KiB
TypeScript
376 行
12 KiB
TypeScript
/**
|
|
* OmniCard AI (CRMmp) - 展会团队战报测试套件 (TDD Red -> Green)
|
|
*
|
|
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 pages/owner/team/overview 设计编写
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const OVERVIEW_PAGE = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/owner/team/overview.uvue'
|
|
);
|
|
const OVERVIEW_UTS = path.resolve(
|
|
__dirname,
|
|
'../../../mpuniapp/pages/owner/team/useOverview.uts'
|
|
);
|
|
|
|
type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived';
|
|
|
|
type LeadEntity = {
|
|
id: string | number;
|
|
cardId: string | number;
|
|
name: string;
|
|
company?: string;
|
|
status: LeadStatus;
|
|
intentScore: number;
|
|
source: 'nfc' | 'qr' | 'link' | 'ai_chat';
|
|
};
|
|
|
|
type RadarTrack = {
|
|
id: string | number;
|
|
cardId: string | number;
|
|
visitorId: string;
|
|
touchType: 'nfc' | 'qr' | 'link';
|
|
stayDurationMs: number;
|
|
scrollDepth: number;
|
|
viewedBlocks: string[];
|
|
country?: string;
|
|
city?: string;
|
|
};
|
|
|
|
type OwnerPageQuery = {
|
|
page: number;
|
|
size: number;
|
|
cardId?: string | number;
|
|
};
|
|
|
|
type OwnerPageData<T> = {
|
|
list: T[];
|
|
pagination: {
|
|
page: number;
|
|
size: number;
|
|
total: number;
|
|
};
|
|
};
|
|
|
|
type OverviewSummary = {
|
|
totalLeads: number;
|
|
hotCount: number;
|
|
totalTracks: number;
|
|
topCountry: string;
|
|
};
|
|
|
|
type LoadOverviewApi = {
|
|
fetchOwnerLeadPage?: (
|
|
query: OwnerPageQuery
|
|
) => Promise<OwnerPageData<LeadEntity>>;
|
|
fetchOwnerRadarPage?: (
|
|
query: OwnerPageQuery
|
|
) => Promise<OwnerPageData<RadarTrack>>;
|
|
};
|
|
|
|
type LoadOverviewResult = {
|
|
leads: LeadEntity[];
|
|
tracks: RadarTrack[];
|
|
summary: OverviewSummary;
|
|
empty: boolean;
|
|
errorMessage: string;
|
|
};
|
|
|
|
type LoadOverviewInput = {
|
|
page?: number;
|
|
size?: number;
|
|
cardId?: string | number;
|
|
};
|
|
|
|
type UseOverviewModule = {
|
|
summarize: (leads: LeadEntity[], tracks: RadarTrack[]) => OverviewSummary;
|
|
loadOverview: (
|
|
input?: LoadOverviewInput,
|
|
api?: LoadOverviewApi
|
|
) => Promise<LoadOverviewResult>;
|
|
};
|
|
|
|
const EMOJI_RE = /[\u2600-\u27BF\uFE0F]|[\uD83C-\uDBFF][\uDC00-\uDFFF]/;
|
|
|
|
function buildLead(overrides?: Partial<LeadEntity>): LeadEntity {
|
|
return {
|
|
id: 21,
|
|
cardId: 8,
|
|
name: 'Alex Vance',
|
|
company: 'Nexus Tech',
|
|
status: 'new',
|
|
intentScore: 72,
|
|
source: 'nfc',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildTrack(overrides?: Partial<RadarTrack>): RadarTrack {
|
|
return {
|
|
id: 81,
|
|
cardId: 8,
|
|
visitorId: 'vis_ae_1201',
|
|
country: 'United Arab Emirates',
|
|
city: 'Dubai',
|
|
touchType: 'nfc',
|
|
stayDurationMs: 45000,
|
|
scrollDepth: 72,
|
|
viewedBlocks: ['hero', 'case'],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildPage<T>(
|
|
list: T[],
|
|
pagination?: Partial<OwnerPageData<T>['pagination']>
|
|
): OwnerPageData<T> {
|
|
return {
|
|
list,
|
|
pagination: {
|
|
page: 1,
|
|
size: 20,
|
|
total: list.length,
|
|
...pagination,
|
|
},
|
|
};
|
|
}
|
|
|
|
function loadUseOverview(): UseOverviewModule {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
return require(OVERVIEW_UTS) as UseOverviewModule;
|
|
}
|
|
|
|
describe('OmniCard AI 展会团队战报 (useOverview.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. summarize 统计正确', () => {
|
|
it('1.1 汇总线索总数、高意向数、足迹数与主要国家', () => {
|
|
const { summarize } = loadUseOverview();
|
|
const leads: LeadEntity[] = [
|
|
buildLead({ id: 1, status: 'hot' }),
|
|
buildLead({ id: 2, status: 'new' }),
|
|
buildLead({ id: 3, status: 'hot' }),
|
|
buildLead({ id: 4, status: 'won' }),
|
|
];
|
|
const tracks: RadarTrack[] = [
|
|
buildTrack({ id: 11, country: 'Germany' }),
|
|
buildTrack({ id: 12, country: 'United Arab Emirates' }),
|
|
buildTrack({ id: 13, country: 'United Arab Emirates' }),
|
|
buildTrack({ id: 14, country: 'Germany' }),
|
|
buildTrack({ id: 15, country: 'United Arab Emirates' }),
|
|
];
|
|
|
|
const summary = summarize(leads, tracks);
|
|
|
|
expect(summary.totalLeads).toBe(4);
|
|
expect(summary.hotCount).toBe(2);
|
|
expect(summary.totalTracks).toBe(5);
|
|
expect(summary.topCountry).toBe('United Arab Emirates');
|
|
});
|
|
|
|
it('1.2 空列表归零,忽略空白国家,平局取先出现的国家', () => {
|
|
const { summarize } = loadUseOverview();
|
|
|
|
const empty = summarize([], []);
|
|
expect(empty.totalLeads).toBe(0);
|
|
expect(empty.hotCount).toBe(0);
|
|
expect(empty.totalTracks).toBe(0);
|
|
expect(empty.topCountry).toBe('');
|
|
|
|
const tied = summarize(
|
|
[buildLead({ id: 9, status: 'contacted' })],
|
|
[
|
|
buildTrack({ id: 21, country: ' ' }),
|
|
buildTrack({ id: 22, country: 'Japan' }),
|
|
buildTrack({ id: 23, country: 'France' }),
|
|
buildTrack({ id: 24, country: undefined }),
|
|
buildTrack({ id: 25, country: 'France' }),
|
|
buildTrack({ id: 26, country: 'Japan' }),
|
|
]
|
|
);
|
|
expect(tied.totalLeads).toBe(1);
|
|
expect(tied.hotCount).toBe(0);
|
|
expect(tied.totalTracks).toBe(6);
|
|
expect(tied.topCountry).toBe('Japan');
|
|
});
|
|
});
|
|
|
|
describe('2. loadOverview 调两个 API', () => {
|
|
it('2.1 并行注入 fetchOwnerLeadPage 与 fetchOwnerRadarPage,默认 size 为 20', async () => {
|
|
const { loadOverview } = loadUseOverview();
|
|
const leads = [
|
|
buildLead({ id: 1, status: 'hot', name: 'Mina Cole' }),
|
|
buildLead({ id: 2, status: 'new', name: 'Omar Said' }),
|
|
];
|
|
const tracks = [
|
|
buildTrack({ id: 31, country: 'Singapore' }),
|
|
buildTrack({ id: 32, country: 'Singapore' }),
|
|
buildTrack({ id: 33, country: 'Spain' }),
|
|
];
|
|
|
|
let leadStarted = false;
|
|
let radarStarted = false;
|
|
let leadSawRadar = false;
|
|
let radarSawLead = false;
|
|
|
|
const fetchOwnerLeadPage = jest.fn(
|
|
async (query: OwnerPageQuery): Promise<OwnerPageData<LeadEntity>> => {
|
|
leadStarted = true;
|
|
leadSawRadar = radarStarted;
|
|
expect(query.page).toBe(1);
|
|
expect(query.size).toBe(20);
|
|
return buildPage(leads, { page: 1, size: 20, total: 2 });
|
|
}
|
|
);
|
|
const fetchOwnerRadarPage = jest.fn(
|
|
async (query: OwnerPageQuery): Promise<OwnerPageData<RadarTrack>> => {
|
|
radarStarted = true;
|
|
radarSawLead = leadStarted;
|
|
expect(query.page).toBe(1);
|
|
expect(query.size).toBe(20);
|
|
return buildPage(tracks, { page: 1, size: 20, total: 3 });
|
|
}
|
|
);
|
|
|
|
const result = await loadOverview(
|
|
{ page: 1, size: 20 },
|
|
{ fetchOwnerLeadPage, fetchOwnerRadarPage }
|
|
);
|
|
|
|
expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1);
|
|
expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1);
|
|
expect(leadSawRadar || radarSawLead).toBe(true);
|
|
expect(result.leads).toHaveLength(2);
|
|
expect(result.tracks).toHaveLength(3);
|
|
expect(result.summary.totalLeads).toBe(2);
|
|
expect(result.summary.hotCount).toBe(1);
|
|
expect(result.summary.totalTracks).toBe(3);
|
|
expect(result.summary.topCountry).toBe('Singapore');
|
|
expect(result.empty).toBe(false);
|
|
expect(result.errorMessage).toBe('');
|
|
});
|
|
|
|
it('2.2 无入参时仍调用两个 API,且无注入依赖时走默认实现', async () => {
|
|
const { loadOverview } = loadUseOverview();
|
|
const fetchOwnerLeadPage = jest.fn(
|
|
async (): Promise<OwnerPageData<LeadEntity>> => buildPage([])
|
|
);
|
|
const fetchOwnerRadarPage = jest.fn(
|
|
async (): Promise<OwnerPageData<RadarTrack>> => buildPage([])
|
|
);
|
|
|
|
const result = await loadOverview(undefined, {
|
|
fetchOwnerLeadPage,
|
|
fetchOwnerRadarPage,
|
|
});
|
|
|
|
expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1);
|
|
expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1);
|
|
expect(fetchOwnerLeadPage).toHaveBeenCalledWith(
|
|
expect.objectContaining({ page: 1, size: 20 })
|
|
);
|
|
expect(fetchOwnerRadarPage).toHaveBeenCalledWith(
|
|
expect.objectContaining({ page: 1, size: 20 })
|
|
);
|
|
expect(result.empty).toBe(true);
|
|
expect(result.errorMessage).toBe('');
|
|
expect(result.summary.totalLeads).toBe(0);
|
|
expect(result.summary.totalTracks).toBe(0);
|
|
});
|
|
|
|
it('2.3 任一接口失败写入错误,不伪装成功汇总', async () => {
|
|
const { loadOverview } = loadUseOverview();
|
|
const fetchOwnerLeadPage = jest.fn(
|
|
async (): Promise<OwnerPageData<LeadEntity>> => {
|
|
throw new Error('lead page unavailable');
|
|
}
|
|
);
|
|
const fetchOwnerRadarPage = jest.fn(
|
|
async (): Promise<OwnerPageData<RadarTrack>> =>
|
|
buildPage([buildTrack()])
|
|
);
|
|
|
|
const result = await loadOverview(
|
|
{ size: 20 },
|
|
{ fetchOwnerLeadPage, fetchOwnerRadarPage }
|
|
);
|
|
|
|
expect(fetchOwnerLeadPage).toHaveBeenCalledTimes(1);
|
|
expect(fetchOwnerRadarPage).toHaveBeenCalledTimes(1);
|
|
expect(result.leads).toEqual([]);
|
|
expect(result.tracks).toEqual([]);
|
|
expect(result.summary.totalLeads).toBe(0);
|
|
expect(result.summary.hotCount).toBe(0);
|
|
expect(result.summary.totalTracks).toBe(0);
|
|
expect(result.summary.topCountry).toBe('');
|
|
expect(result.empty).toBe(false);
|
|
expect(result.errorMessage).toBe('lead page unavailable');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('OmniCard AI 展会团队战报页契约 (overview.uvue)', () => {
|
|
it('页面存在且含数字摘要、list-view 与刷新主 CTA', () => {
|
|
expect(fs.existsSync(OVERVIEW_PAGE)).toBe(true);
|
|
expect(fs.existsSync(OVERVIEW_UTS)).toBe(true);
|
|
|
|
const pageSource = fs.readFileSync(OVERVIEW_PAGE, 'utf8');
|
|
const logicSource = fs.readFileSync(OVERVIEW_UTS, 'utf8');
|
|
const combined = `${pageSource}\n${logicSource}`;
|
|
|
|
expect(pageSource.includes('script setup lang="uts"')).toBe(true);
|
|
expect(pageSource.includes('<list-view')).toBe(true);
|
|
expect(pageSource.includes('<list-item')).toBe(true);
|
|
expect(pageSource.includes('length > 5') || logicSource.includes('length > 5')).toBe(
|
|
true
|
|
);
|
|
expect(combined.includes('summarize')).toBe(true);
|
|
expect(combined.includes('loadOverview')).toBe(true);
|
|
expect(combined.includes('fetchOwnerLeadPage')).toBe(true);
|
|
expect(combined.includes('fetchOwnerRadarPage')).toBe(true);
|
|
expect(logicSource.includes('Promise.all')).toBe(true);
|
|
expect(pageSource.includes('getOmniText')).toBe(true);
|
|
expect(combined.includes('crm.leads')).toBe(true);
|
|
expect(combined.includes('radar.visitor_trace')).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('#FFFFFF')).toBe(true);
|
|
});
|
|
|
|
it('无 emoji、无 any,模板无硬编码中文主文案,长列表不用 view v-for', () => {
|
|
const pageSource = fs.readFileSync(OVERVIEW_PAGE, 'utf8');
|
|
const logicSource = fs.readFileSync(OVERVIEW_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(/<view[^>]*v-for/.test(template)).toBe(false);
|
|
expect(/[\u4e00-\u9fff]/.test(template)).toBe(false);
|
|
expect(pageSource.includes('innerAudioContext') || pageSource.includes('autoplay')).toBe(
|
|
false
|
|
);
|
|
});
|
|
});
|