- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
206 行
7.8 KiB
TypeScript
206 行
7.8 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { BaseController } from '@cool-midway/core';
|
|
import { CardCasesEntity } from '../../../src/modules/card/entity/cases';
|
|
import { CardCasesService } from '../../../src/modules/card/service/cases';
|
|
import {
|
|
AdminCardCasesController,
|
|
adminCardCasesCurdOption,
|
|
} from '../../../src/modules/card/controller/admin/cases';
|
|
|
|
describe('后台出海案例作品集 (Admin Card Cases CRUD)', () => {
|
|
const entityFile = path.join(
|
|
__dirname,
|
|
'../../../src/modules/card/entity/cases.ts'
|
|
);
|
|
const serviceFile = path.join(
|
|
__dirname,
|
|
'../../../src/modules/card/service/cases.ts'
|
|
);
|
|
const controllerFile = path.join(
|
|
__dirname,
|
|
'../../../src/modules/card/controller/admin/cases.ts'
|
|
);
|
|
|
|
describe('1. 契约文件与自动路由约定', () => {
|
|
it('文件路径应符合 /admin/card/cases 路由与模块约定', () => {
|
|
expect(fs.existsSync(entityFile)).toBe(true);
|
|
expect(fs.existsSync(serviceFile)).toBe(true);
|
|
expect(fs.existsSync(controllerFile)).toBe(true);
|
|
});
|
|
|
|
it('控制器应继承 BaseController 并挂载 CoolController CRUD', () => {
|
|
expect(AdminCardCasesController.prototype).toBeInstanceOf(BaseController);
|
|
expect(adminCardCasesCurdOption.entity).toBe(CardCasesEntity);
|
|
expect(adminCardCasesCurdOption.service).toBe(CardCasesService);
|
|
});
|
|
|
|
it('装饰器 api 必须包含 add/delete/update/info/list/page', () => {
|
|
expect(adminCardCasesCurdOption.api).toEqual([
|
|
'add',
|
|
'delete',
|
|
'update',
|
|
'info',
|
|
'list',
|
|
'page',
|
|
]);
|
|
});
|
|
|
|
it('pageQueryOp 应支持 a.cardId 精确筛选与 title/client/metrics 模糊搜索', () => {
|
|
const pageQueryOp = adminCardCasesCurdOption.pageQueryOp;
|
|
expect(pageQueryOp.fieldEq).toEqual(expect.arrayContaining(['a.cardId']));
|
|
expect(pageQueryOp.keyWordLikeFields).toEqual(
|
|
expect.arrayContaining(['a.title', 'a.client', 'a.metrics'])
|
|
);
|
|
expect(pageQueryOp.addOrderBy).toMatchObject({ sortOrder: 'asc' });
|
|
});
|
|
});
|
|
|
|
describe('2. 实体字段契约 (CardCasesEntity)', () => {
|
|
it('实体实例应支持出海案例必需契约字段', () => {
|
|
const item = new CardCasesEntity();
|
|
item.cardId = 1001;
|
|
item.title = '智能电动滑板车欧美 DTC 出海';
|
|
item.client = 'VoltRide Global';
|
|
item.duration = '2024.01 - 2024.06';
|
|
item.metrics = '+320% ROI, 45,000+ 留资量';
|
|
item.coverImage = 'https://cdn.example.com/cases/voltride.webp';
|
|
item.pdfUrl = 'https://cdn.example.com/cases/voltride-deck.pdf';
|
|
item.description = '品牌从 0 到 1 进军北美市场独立站及全渠道获客';
|
|
item.sortOrder = 1;
|
|
|
|
expect(item.cardId).toBe(1001);
|
|
expect(item.title).toBe('智能电动滑板车欧美 DTC 出海');
|
|
expect(item.client).toBe('VoltRide Global');
|
|
expect(item.duration).toBe('2024.01 - 2024.06');
|
|
expect(item.metrics).toBe('+320% ROI, 45,000+ 留资量');
|
|
expect(item.coverImage).toBe('https://cdn.example.com/cases/voltride.webp');
|
|
expect(item.pdfUrl).toBe('https://cdn.example.com/cases/voltride-deck.pdf');
|
|
expect(item.description).toBe('品牌从 0 到 1 进军北美市场独立站及全渠道获客');
|
|
expect(item.sortOrder).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('3. 服务层按 cardId 筛选与 CRUD 逻辑 (CardCasesService)', () => {
|
|
let service: CardCasesService;
|
|
let mockList: CardCasesEntity[] = [];
|
|
|
|
beforeEach(() => {
|
|
mockList = [
|
|
Object.assign(new CardCasesEntity(), {
|
|
id: 1,
|
|
cardId: 101,
|
|
title: '跨境智能硬件众筹出海',
|
|
client: 'TechNova',
|
|
duration: '3个月',
|
|
metrics: '+280% 众筹目标达成',
|
|
coverImage: 'https://cdn.example.com/case1.png',
|
|
pdfUrl: 'https://cdn.example.com/case1.pdf',
|
|
sortOrder: 2,
|
|
}),
|
|
Object.assign(new CardCasesEntity(), {
|
|
id: 2,
|
|
cardId: 101,
|
|
title: '东南亚 TikTok 跨境直播获客',
|
|
client: 'FashionGo',
|
|
duration: '6个月',
|
|
metrics: '千万级曝光, +150% GMV',
|
|
coverImage: 'https://cdn.example.com/case2.png',
|
|
pdfUrl: 'https://cdn.example.com/case2.pdf',
|
|
sortOrder: 1,
|
|
}),
|
|
Object.assign(new CardCasesEntity(), {
|
|
id: 3,
|
|
cardId: 102,
|
|
title: 'B2B 重工机械中东拓客',
|
|
client: 'HeavyMach',
|
|
duration: '12个月',
|
|
metrics: '$5M+ 意向大单',
|
|
coverImage: 'https://cdn.example.com/case3.png',
|
|
pdfUrl: 'https://cdn.example.com/case3.pdf',
|
|
sortOrder: 1,
|
|
}),
|
|
];
|
|
|
|
service = new CardCasesService();
|
|
service.cardCasesEntity = {
|
|
find: jest.fn().mockImplementation(async (options?: { where?: { cardId?: number }; order?: Record<string, string> }) => {
|
|
let res = [...mockList];
|
|
if (options?.where?.cardId !== undefined) {
|
|
res = res.filter(item => Number(item.cardId) === Number(options.where.cardId));
|
|
}
|
|
if (options?.order?.sortOrder === 'ASC') {
|
|
res.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
}
|
|
return res;
|
|
}),
|
|
findOneBy: jest.fn().mockImplementation(async (where: { id: number }) => {
|
|
return mockList.find(item => item.id === where.id) || null;
|
|
}),
|
|
save: jest.fn().mockImplementation(async (entity: Partial<CardCasesEntity>) => {
|
|
if (entity.id) {
|
|
const index = mockList.findIndex(item => item.id === entity.id);
|
|
if (index >= 0) {
|
|
mockList[index] = Object.assign(mockList[index], entity);
|
|
return mockList[index];
|
|
}
|
|
}
|
|
const created = Object.assign(new CardCasesEntity(), entity, {
|
|
id: mockList.length + 1,
|
|
});
|
|
mockList.push(created);
|
|
return created;
|
|
}),
|
|
delete: jest.fn().mockImplementation(async (id: number) => {
|
|
mockList = mockList.filter(item => item.id !== id);
|
|
return { affected: 1 };
|
|
}),
|
|
} as unknown as typeof service.cardCasesEntity;
|
|
});
|
|
|
|
it('getCasesByCardId 应根据名片 cardId 筛选并按 sortOrder 升序排列', async () => {
|
|
const cases = await service.getCasesByCardId(101);
|
|
expect(cases).toHaveLength(2);
|
|
expect(cases[0].title).toBe('东南亚 TikTok 跨境直播获客');
|
|
expect(cases[0].sortOrder).toBe(1);
|
|
expect(cases[1].title).toBe('跨境智能硬件众筹出海');
|
|
expect(cases[1].sortOrder).toBe(2);
|
|
});
|
|
|
|
it('getCasesByCardId 传入无效 cardId 时应返回空数组', async () => {
|
|
const cases = await service.getCasesByCardId(0);
|
|
expect(cases).toEqual([]);
|
|
});
|
|
|
|
it('创建案例并能查询出最新案例', async () => {
|
|
const created = await service.cardCasesEntity.save({
|
|
cardId: 101,
|
|
title: '拉美电商本土化品牌出海',
|
|
client: 'LatamDirect',
|
|
duration: '4个月',
|
|
metrics: '+90% 复购率',
|
|
coverImage: 'https://cdn.example.com/case4.png',
|
|
pdfUrl: 'https://cdn.example.com/case4.pdf',
|
|
sortOrder: 3,
|
|
});
|
|
|
|
expect(created.id).toBeDefined();
|
|
expect(created.title).toBe('拉美电商本土化品牌出海');
|
|
|
|
const found = await service.cardCasesEntity.findOneBy({ id: created.id });
|
|
expect(found).not.toBeNull();
|
|
expect(found?.client).toBe('LatamDirect');
|
|
});
|
|
|
|
it('更新案例信息应成功', async () => {
|
|
await service.cardCasesEntity.save({
|
|
id: 1,
|
|
metrics: '+350% 众筹目标达成',
|
|
});
|
|
|
|
const updated = await service.cardCasesEntity.findOneBy({ id: 1 });
|
|
expect(updated?.metrics).toBe('+350% 众筹目标达成');
|
|
});
|
|
});
|
|
});
|