import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import defaultI18n from '../src/locale/index.ts'; import { routes } from '../src/routes.ts'; const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const articlesPagePath = path.join(adminRoot, 'src/pages/card/articles/index.tsx'); const articlesLocalePath = path.join(adminRoot, 'src/pages/card/articles/locale/index.ts'); test('1. card/articles 路由必须在 routes.ts 注册,且多语言包含 menu.card.articles', () => { const cardGroup = routes.find((r) => r.key === 'card'); assert.ok(cardGroup, 'routes.ts 必须存在 card 顶级路由'); const articlesRoute = cardGroup.children?.find((c) => c.key === 'card/articles'); assert.ok(articlesRoute, 'routes.ts 中 card 分组下必须存在 card/articles 路由'); assert.equal(articlesRoute?.name, 'menu.card.articles'); assert.ok( (defaultI18n['zh-CN'] as Record)['menu.card.articles'], 'zh-CN 必须包含 menu.card.articles' ); assert.ok( (defaultI18n['en-US'] as Record)['menu.card.articles'], 'en-US 必须包含 menu.card.articles' ); }); test('2. card/articles 页面必须请求 article API,并严格使用 Arco Table/Drawer/Form/Input/Switch', () => { assert.ok(fs.existsSync(articlesPagePath), '文章管理页面文件必须存在'); const content = fs.readFileSync(articlesPagePath, 'utf-8'); // API 路径断言 assert.ok( content.includes('admin/card/article/page'), '必须请求 POST /api/admin/card/article/page' ); assert.ok( content.includes('admin/card/article/add'), '必须请求 POST /api/admin/card/article/add' ); assert.ok( content.includes('admin/card/article/update'), '必须请求 POST /api/admin/card/article/update' ); assert.ok( content.includes('admin/card/article/delete'), '必须请求 POST /api/admin/card/article/delete' ); // Arco 组件断言 assert.ok(content.includes('Table'), '必须使用 Arco Table'); assert.ok(content.includes('Drawer'), '必须使用 Arco Drawer'); assert.ok(content.includes('Form'), '必须使用 Arco Form'); assert.ok(content.includes('Input'), '必须使用 Arco Input'); assert.ok(content.includes('Switch'), '必须使用 Arco Switch'); assert.ok(content.includes('Popconfirm'), '删除确认必须使用 Arco Popconfirm'); assert.ok( content.includes("from '@arco-design/web-react'"), '必须从 @arco-design/web-react 引入组件' ); // 代码规范断言 assert.ok(content.includes('CardArticle'), '必须使用 CardArticle 类型'); assert.ok( content.includes('code === 1000') || content.includes('code ===1000'), '必须按 Cool-Admin code === 1000 判断成功' ); assert.ok(content.includes('Message.error'), '失败必须使用 Arco Message.error'); assert.ok(!content.includes('Mock.js') && !content.includes("from 'mockjs'"), '禁止使用 Mock.js 假数据'); assert.ok(!/\bany\b/.test(content), '禁止使用 any'); assert.ok(!content.includes(' { const content = fs.readFileSync(articlesPagePath, 'utf-8'); assert.ok(content.includes('lightColors'), '颜色必须引用 lightColors token'); assert.ok(!/#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})\b/.test(content), '禁止硬编码颜色 (#hex)'); // 单一主 CTA const primaryButtonMatches = content.match(/type=["']primary["']/g) || []; assert.equal( primaryButtonMatches.length, 1, '遵循瑞士国际主义风格:全屏仅允许 1 个 type="primary" 主 CTA' ); // 禁止 emoji 正则测试 const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; assert.ok(!emojiRegex.test(content), '禁止使用 emoji'); // 支持富文本/Markdown 草稿/发布编辑与 OpenGraph 社交预览卡 assert.ok( content.includes('ogPreview') || content.includes('OpenGraph') || content.includes('ogMeta'), '必须支持 OpenGraph 社交卡片预览联动' ); assert.ok(content.includes('isPublished'), '必须支持发布状态草稿/上架控制'); }); test('4. card/articles 语言包必须包含中英文案与 OpenGraph 预览词条', () => { assert.ok(fs.existsSync(articlesLocalePath), '语言包文件必须存在'); const content = fs.readFileSync(articlesLocalePath, 'utf-8'); assert.ok(content.includes("'en-US'"), '必须包含 en-US'); assert.ok(content.includes("'zh-CN'"), '必须包含 zh-CN'); assert.ok(content.includes("'ar-SA'"), '必须包含 ar-SA'); assert.ok(content.includes('cardArticles.title'), '必须包含页面标题文案'); assert.ok(content.includes('cardArticles.action.add'), '必须包含新增文章主 CTA 文案'); assert.ok(content.includes('cardArticles.ogPreview.title'), '必须包含 OG 社交预览卡文案'); assert.ok( content.includes('cardArticles.form.cardId.placeholder'), '必须包含名片编号占位文案' ); assert.ok( content.includes('cardArticles.form.title.placeholder'), '必须包含标题占位文案' ); assert.ok( content.includes('cardArticles.form.summary.placeholder'), '必须包含摘要占位文案' ); assert.ok( content.includes('cardArticles.form.content.placeholder'), '必须包含正文占位文案' ); assert.ok(!/\bany\b/.test(content), '语言包禁止使用 any'); }); test('4b. card/articles 表单占位必须走 locale,禁止硬编码中文 placeholder', () => { const content = fs.readFileSync(articlesPagePath, 'utf-8'); assert.ok( content.includes("t['cardArticles.form.cardId.placeholder']"), 'cardId 占位必须使用 locale' ); assert.ok( content.includes("t['cardArticles.form.title.placeholder']"), '标题占位必须使用 locale' ); assert.ok( content.includes("t['cardArticles.form.summary.placeholder']"), '摘要占位必须使用 locale' ); assert.ok( content.includes("t['cardArticles.form.content.placeholder']"), '正文占位必须使用 locale' ); assert.ok( !/placeholder=["'][^"']*[\u4e00-\u9fff]/.test(content), '禁止硬编码含中文的 placeholder' ); assert.ok(!content.includes('placeholder="101"'), '禁止硬编码 cardId 示例占位'); }); test('5. card/articles 必须支持 OpenGraph 社交卡片多比例切换 (1.91:1 / 1:1)', () => { const content = fs.readFileSync(articlesPagePath, 'utf-8'); assert.ok(content.includes('Radio'), '必须使用 Arco Radio 组件进行卡片比例切换'); assert.ok(content.includes('ogRatio') || content.includes('square'), '必须支持 square 方版与横版比例切换状态'); const localeContent = fs.readFileSync(articlesLocalePath, 'utf-8'); assert.ok(localeContent.includes('cardArticles.ogPreview.landscape'), '语言包必须包含横版比例文案'); assert.ok(localeContent.includes('cardArticles.ogPreview.square'), '语言包必须包含方版比例文案'); }); test('6. 无封面时 OG 预览必须显示 locale 空图占位,禁止默认假图/假 CDN/示例封面', () => { const content = fs.readFileSync(articlesPagePath, 'utf-8'); const localeContent = fs.readFileSync(articlesLocalePath, 'utf-8'); assert.ok( content.includes("t['cardArticles.ogPreview.emptyImage']"), '无封面时必须使用 locale 空图占位文案' ); assert.ok(!content.includes('>OG Image<'), '禁止硬编码 OG Image 占位文案'); assert.ok( /ogPreview\.image\s*\?/.test(content) || /\{ogPreview\.image\s*\?/.test(content), '必须仅在有真实封面 URL 时渲染 img' ); // 禁止默认假图、假 CDN、示例封面地址 const fakeImagePatterns = [ /unsplash\.com/i, /picsum\.photos/i, /placeholder\.com/i, /placehold\.co/i, /via\.placeholder/i, /dummyimage\.com/i, /loremflickr\.com/i, /example\.com\/.*\.(png|jpe?g|webp|gif)/i, /cdn\.example/i, /default[-_]?cover/i, /sample[-_]?cover/i, /fake[-_]?cover/i, /src=["']https?:\/\/[^"']*cover[^"']*["']/i, /src=["']\/assets\/[^"']*cover/i, ]; for (const pattern of fakeImagePatterns) { assert.ok(!pattern.test(content), `页面禁止假图/示例封面地址: ${pattern}`); } assert.ok( localeContent.includes('cardArticles.ogPreview.emptyImage'), '语言包必须包含空图占位文案 key' ); // 三语均需有 emptyImage 词条 const emptyImageMatches = localeContent.match(/cardArticles\.ogPreview\.emptyImage/g) || []; assert.ok( emptyImageMatches.length >= 3, 'en-US / zh-CN / ar-SA 均须包含 emptyImage 词条' ); });