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 { routes } from '../src/routes.ts'; import defaultI18n from '../src/locale/index.ts'; const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const radarMapPagePath = path.join(adminRoot, 'src/pages/dashboard/radar-map/index.tsx'); const radarMapLocalePath = path.join(adminRoot, 'src/pages/dashboard/radar-map/locale/index.ts'); function flattenRouteKeys(items: typeof routes): string[] { return items.flatMap((item) => [ item.key, ...(item.children ? flattenRouteKeys(item.children) : []), ]); } test('1. routes.ts 必须在 dashboard 分组下注册 dashboard/radar-map', () => { const keys = flattenRouteKeys(routes); assert.ok(keys.includes('dashboard/radar-map'), 'routes.ts 必须包含 dashboard/radar-map 路由'); const dashboardGroup = routes.find((item) => item.key === 'dashboard'); assert.ok(dashboardGroup, '应存在 dashboard 路由分组'); assert.ok( dashboardGroup?.children?.some( (child) => child.key === 'dashboard/radar-map' && child.name === 'menu.dashboard.radarMap' ), 'dashboard 分组下必须包含 menu.dashboard.radarMap (dashboard/radar-map)' ); }); test('2. 主词典 index.ts 必须同步包含 menu.dashboard.radarMap 的中英文案', () => { assert.ok( defaultI18n['en-US']['menu.dashboard.radarMap'], 'en-US 主词典必须包含 menu.dashboard.radarMap' ); assert.ok( defaultI18n['zh-CN']['menu.dashboard.radarMap'], 'zh-CN 主词典必须包含 menu.dashboard.radarMap' ); assert.equal(defaultI18n['zh-CN']['menu.dashboard.radarMap'], '全球访客雷达'); }); test('3. 大屏页面必须存在且使用 Arco Design 官方核心组件', () => { assert.ok(fs.existsSync(radarMapPagePath), 'radar-map/index.tsx 页面文件必须存在'); const content = fs.readFileSync(radarMapPagePath, 'utf-8'); // 组件引入校验 assert.ok(content.includes("from '@arco-design/web-react'"), '必须从 @arco-design/web-react 引入组件'); assert.ok(content.includes('Card'), '必须使用 Arco Card'); assert.ok(content.includes('Grid') || (content.includes('Row') && content.includes('Col')), '必须使用 Arco Grid (Row, Col)'); assert.ok(content.includes('Spin'), '必须使用 Arco Spin 加载反馈'); assert.ok(content.includes('Tag'), '必须使用 Arco Tag 状态与排名标签'); assert.ok(content.includes('Table'), '必须使用 Arco Table 展示国家排行'); assert.ok(content.includes('Progress'), '必须使用 Arco Progress 展示占比进度条'); }); test('4. 大屏页面必须对接雷达热力聚合 API 且只设置 1 个主 CTA', () => { const content = fs.readFileSync(radarMapPagePath, 'utf-8'); // 国家/地区分布必须来自已有热力聚合接口(analytics 内含 countryDistribution,或独立 distribution) assert.ok( content.includes('/api/admin/crm/radar/analytics') || content.includes('/api/admin/crm/radar/distribution'), '必须对接雷达热力聚合 analytics 或 distribution API' ); assert.ok( content.includes('countryDistribution') || content.includes('/radar/distribution'), '国家/地区分布必须绑定接口返回的 countryDistribution(或 distribution)' ); // 瑞士国际主义与一屏单主 CTA 原则: 统计 type="primary" 按钮数量 const primaryButtonMatches = content.match(/type=["']primary["']/g) || []; assert.equal( primaryButtonMatches.length, 1, '一屏只允许存在 1 个主 CTA 按钮 (type="primary")' ); }); test('4b. 国家分布禁止写死占比;请求失败或无数据必须空态', () => { const content = fs.readFileSync(radarMapPagePath, 'utf-8'); const localeContent = fs.readFileSync(radarMapLocalePath, 'utf-8'); // 禁止演示用硬编码国家名与写死占比 const bannedCountries = [ 'United States', 'United Kingdom', 'Singapore', 'Germany', 'Japan', 'China', 'Dubai', 'Saudi Arabia', ]; for (const name of bannedCountries) { assert.ok(!content.includes(name), `禁止硬编码国家/地区名: ${name}`); } assert.ok( !/percentage\s*:\s*\d+/.test(content), '禁止在页面中写死 percentage 国家占比' ); assert.ok( !/visitorCount\s*:\s*[1-9]/.test(content), '禁止在页面中写死非零 visitorCount 演示数据' ); // 失败或非 1000 必须清空分布,不得残留假占比 assert.ok( content.includes('countryDistribution: []') || content.includes('EMPTY_ANALYTICS') || content.includes('emptyAnalytics'), '必须具备空的 countryDistribution 初始/空态结构' ); assert.ok( /setAnalyticsData\(\s*(EMPTY_ANALYTICS|emptyAnalytics)/.test(content) || /setAnalyticsData\(\s*\{\s*[\s\S]*countryDistribution\s*:\s*\[\s*\]/.test(content), '接口失败必须 setAnalyticsData 清空为国家空分布' ); // 空态文案走 locale assert.ok( content.includes('radarMap.status.empty') || content.includes("t['radarMap.status.empty']"), '空态必须引用 radarMap.status.empty' ); assert.ok( content.includes('Empty') || content.includes('noDataElement'), '无数据必须使用 Empty 或 Table noDataElement' ); assert.ok(localeContent.includes('radarMap.status.empty'), 'locale 必须包含空态文案'); assert.ok(localeContent.includes('radarMap.status.loadFailed'), 'locale 必须包含加载失败文案'); assert.ok( localeContent.includes('radarMap.country.unknown'), 'locale 必须包含未知国家/地区文案' ); }); test('5. 必须使用设计 Token 系统且保持严谨纯粹 (零 any、零 emoji)', () => { const content = fs.readFileSync(radarMapPagePath, 'utf-8'); // 设计 Token assert.ok( content.includes('@/style/tokens') || content.includes('tokens'), '必须引用规范设计 Token 系统' ); // 严格 TypeScript,禁止 any assert.ok(!content.includes(': any'), '必须严格定义 TypeScript 类型,严禁使用 : any'); assert.ok(!content.includes('as any'), '必须严格定义 TypeScript 类型,严禁使用 as any'); // 严禁 Emoji 字符 const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; assert.ok(!emojiRegex.test(content), '页面中严禁硬编码或使用 Emoji 符号'); }); test('6. 页面专属国际化 locale 必须完整且中英文对齐', () => { assert.ok(fs.existsSync(radarMapLocalePath), 'radar-map/locale/index.ts 必须存在'); const localeContent = fs.readFileSync(radarMapLocalePath, 'utf-8'); // 检查关键字典项 assert.ok(localeContent.includes("'en-US'"), '必须包含 en-US'); assert.ok(localeContent.includes("'zh-CN'"), '必须包含 zh-CN'); assert.ok(localeContent.includes('radarMap.title'), '必须包含 radarMap.title'); assert.ok(localeContent.includes('radarMap.topCountries'), '必须包含 radarMap.topCountries'); assert.ok(localeContent.includes('radarMap.deviceAnalytics'), '必须包含 radarMap.deviceAnalytics'); // 国际化文案中严禁 Emoji const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; assert.ok(!emojiRegex.test(localeContent), '国际化语言包中严禁出现 Emoji 符号'); });