import { MOCK_CARDS_DATA, MockCardBundle } from '../../../src/modules/card/mock/seed-cards'; import { CardPortalService } from '../../../src/modules/card/service/portal'; import { CardSeoService } from '../../../src/modules/card/service/seo'; import { CardInfoEntity } from '../../../src/modules/card/entity/info'; import { CardBentoEntity } from '../../../src/modules/card/entity/bento'; describe('多张多媒体名片数据 (配图与配视频) 规格与集成测试', () => { it('1. 应包含至少 6 组不同国际顶级行业的高保真名片数据包且 Slug 唯一', () => { expect(Array.isArray(MOCK_CARDS_DATA)).toBe(true); expect(MOCK_CARDS_DATA.length).toBeGreaterThanOrEqual(6); const slugs = MOCK_CARDS_DATA.map(item => item.card.slug); const uniqueSlugs = new Set(slugs); expect(uniqueSlugs.size).toBe(MOCK_CARDS_DATA.length); // Slug 必须唯一 // 必须包含涵盖 6 大赛道的关键人物 slug expect(slugs).toContain('alex-zheng'); // 消费电子硬件 expect(slugs).toContain('elena-rostova'); // 云原生与大模型架构 expect(slugs).toContain('tariq-al-mansoor'); // 中东智慧基建与绿色能源 expect(slugs).toContain('chloe-dubois'); // 巴黎可持续时尚奢品 expect(slugs).toContain('kenji-tanaka'); // 跨国汽车制造与智能物流 expect(slugs).toContain('marcus-weber'); // 瑞士生物医药与数字健康 }); it('2. 每张名片的基础信息必须完整且配有高分辨率头像与国际化联系方式', () => { MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => { const { card } = bundle; expect(card.id).toBeDefined(); expect(card.name).toBeTruthy(); expect(card.title).toBeTruthy(); expect(card.company).toBeTruthy(); expect(card.slogan).toBeTruthy(); expect(card.status).toBe('published'); // 配图验证:头像必须是合法图片 URL (支持扩展名与真实 Unsplash 原生路径) const validImagePattern = /^https?:\/\/.+(\.(png|jpe?g|webp|svg)(\?.*)?|\/photo-[a-zA-Z0-9_-]+(\?.*)?)$/i; expect(card.avatarUrl).toMatch(validImagePattern); // 国际化联系方式 expect(card.phone).toMatch(/^\+[1-9]\d{1,14}$/); // E.164 规范 expect(card.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); expect(card.timezone).toBeTruthy(); expect(card.audioPronunciationUrl).toMatch(/^https?:\/\/.+\.(mp3|wav|ogg|m4a)$/i); }); }); it('3. 每张名片必须包含丰富的 Bento Grid 块 (>= 5 个),包含视频与社交/留资互动块', () => { MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => { const { bentoBlocks } = bundle; expect(Array.isArray(bentoBlocks)).toBe(true); expect(bentoBlocks.length).toBeGreaterThanOrEqual(5); // 必须包含至少一个 video 类型的 Bento 块 const videoBlocks = bentoBlocks.filter(b => b.type === 'video'); expect(videoBlocks.length).toBeGreaterThanOrEqual(1); videoBlocks.forEach(videoBlock => { expect(videoBlock.title).toBeTruthy(); const validImagePattern = /^https?:\/\/.+(\.(png|jpe?g|webp|svg)(\?.*)?|\/photo-[a-zA-Z0-9_-]+(\?.*)?)$/i; expect(videoBlock.mediaUrl).toMatch(validImagePattern); let videoUrl = videoBlock.linkUrl; if (typeof videoBlock.metaJson === 'object' && videoBlock.metaJson !== null) { videoUrl = (videoBlock.metaJson as any).videoUrl || videoUrl; } else if (typeof videoBlock.metaJson === 'string') { const parsed = JSON.parse(videoBlock.metaJson); videoUrl = parsed.videoUrl || videoUrl; } expect(videoUrl).toMatch(/^https?:\/\/.+\.(mp4|webm|m3u8)(\?.*)?$/i); }); // 必须包含 social 或 form 等交互块 const interactiveBlocks = bentoBlocks.filter(b => b.type === 'social' || b.type === 'form'); expect(interactiveBlocks.length).toBeGreaterThanOrEqual(1); }); }); it('4. 每张名片必须包含图文案例与文章,且配置了真实多媒体封面配图与量化商业指标', () => { const validImagePattern = /^https?:\/\/.+(\.(png|jpe?g|webp|svg)(\?.*)?|\/photo-[a-zA-Z0-9_-]+(\?.*)?)$/i; MOCK_CARDS_DATA.forEach((bundle: MockCardBundle) => { // 检查案例 (每个名片至少 2 个标杆产品相册与案例) expect(Array.isArray(bundle.cases)).toBe(true); expect(bundle.cases.length).toBeGreaterThanOrEqual(2); bundle.cases.forEach(c => { expect(c.title).toBeTruthy(); expect(c.client).toBeTruthy(); expect(c.metrics).toBeTruthy(); // 必须包含量化商业成果 expect(c.description).toBeTruthy(); expect(c.coverImage).toMatch(validImagePattern); // 验证产品相册多图画廊 (metaJson.images 必须存在且至少 3 张高清产品/场景图) let meta = c.metaJson; if (typeof meta === 'string') { meta = JSON.parse(meta); } expect(meta).toBeDefined(); const images = (meta as any)?.images; expect(Array.isArray(images)).toBe(true); expect(images.length).toBeGreaterThanOrEqual(3); images.forEach((img: string) => { expect(img).toMatch(validImagePattern); }); // 验证结构化 KPI (metaJson.kpis 必须存在且至少 2 个指标) const kpis = (meta as any)?.kpis; expect(Array.isArray(kpis)).toBe(true); expect(kpis.length).toBeGreaterThanOrEqual(2); }); // 检查文章 expect(Array.isArray(bundle.articles)).toBe(true); expect(bundle.articles.length).toBeGreaterThanOrEqual(1); bundle.articles.forEach(a => { expect(a.title).toBeTruthy(); expect(a.summary).toBeTruthy(); expect(a.content).toBeTruthy(); expect(a.coverImage).toMatch(validImagePattern); }); }); }); it('5. 微官网服务 (CardPortalService) 能够无缝聚合各名片详情,正确渲染多媒体 Bento 块', async () => { const cardInfoRepo: any = { findOne: jest.fn().mockImplementation(async options => { const slug = options?.where?.slug; const id = options?.where?.id; const found = MOCK_CARDS_DATA.find( b => (slug && b.card.slug === slug) || (id && b.card.id === Number(id)) ); return found ? ({ ...found.card } as CardInfoEntity) : null; }), }; const cardBentoRepo: any = { find: jest.fn().mockImplementation(async options => { const cardId = options?.where?.cardId; const found = MOCK_CARDS_DATA.find(b => b.card.id === Number(cardId)); return found ? found.bentoBlocks.map(blk => ({ ...blk } as CardBentoEntity)) : []; }), }; const portalService = new CardPortalService(); portalService.cardInfoEntity = cardInfoRepo; portalService.cardBentoEntity = cardBentoRepo; CardPortalService.clearAllCache(); for (const bundle of MOCK_CARDS_DATA) { const detail = await portalService.getPortalDetail(bundle.card.slug); expect(detail).toBeDefined(); expect(detail.name).toBe(bundle.card.name); expect(detail.avatarUrl).toBe(bundle.card.avatarUrl); // 验证视频块是否存在于公开微官网聚合结果中 const videoBlock = detail.bentoBlocks.find(b => b.type === 'video'); expect(videoBlock).toBeDefined(); expect(videoBlock?.mediaUrl).toBeTruthy(); } }); it('6. 服务端渲染微官网预览 (CardSeoService.renderCardHtml) 包含完整的 Bento Grid 结构与多媒体', async () => { const seoService = new CardSeoService(); const firstBundle = MOCK_CARDS_DATA[0]; const portalDetail = { ...firstBundle.card, bentoBlocks: firstBundle.bentoBlocks as any[], cases: firstBundle.cases, articles: firstBundle.articles, } as any; const html = seoService.renderCardHtml(portalDetail); expect(html).toContain(''); expect(html).toContain(firstBundle.card.name); // 应包含 Bento Grid 容器类名 expect(html).toContain('bento-grid'); // 应包含 Hero 与 Video 相关卡片内容 expect(html).toContain(firstBundle.bentoBlocks[0].title); expect(html).toContain(firstBundle.cases[0].title); }); it('7. 控制器公开路由 /mock-cards:默认空数组,ALLOW_PORTAL_MOCK=1 才返回演示清单', async () => { const { OpenCardPortalController } = require('../../../src/modules/card/controller/open/portal'); const controller = new OpenCardPortalController(); controller.ok = jest.fn((data: any) => ({ code: 1000, data })); const prevAllow = process.env.ALLOW_PORTAL_MOCK; try { delete process.env.ALLOW_PORTAL_MOCK; const emptyRes = await controller.mockCards(); expect(emptyRes.code).toBe(1000); expect(Array.isArray(emptyRes.data)).toBe(true); expect(emptyRes.data).toEqual([]); expect(JSON.stringify(emptyRes.data)).not.toContain('Alex Zheng'); process.env.ALLOW_PORTAL_MOCK = '1'; const res = await controller.mockCards(); expect(res.code).toBe(1000); expect(Array.isArray(res.data)).toBe(true); expect(res.data.length).toBe(6); expect(res.data[0].slug).toBe('alex-zheng'); expect(res.data[5].slug).toBe('marcus-weber'); expect(res.data[0].bentoBlocksCount).toBeGreaterThanOrEqual(5); } finally { if (prevAllow === undefined) { delete process.env.ALLOW_PORTAL_MOCK; } else { process.env.ALLOW_PORTAL_MOCK = prevAllow; } } }); it('8. seedCardsToDatabase 函数能够幂等同步数据到仓库', async () => { const { seedCardsToDatabase } = require('../../../src/modules/card/mock/seed-cards'); const mockRepo = { findOne: jest.fn().mockResolvedValue(null), create: jest.fn((data: any) => ({ ...data, id: 1 })), save: jest.fn((data: any) => Promise.resolve({ ...data, id: 1 })), delete: jest.fn().mockResolvedValue({ affected: 1 }), }; const mockDataSource = { getRepository: jest.fn().mockReturnValue(mockRepo), }; const insertedCount = await seedCardsToDatabase(mockDataSource); expect(insertedCount).toBe(6); expect(mockRepo.save).toHaveBeenCalled(); expect(mockRepo.delete).toHaveBeenCalled(); }); });