文件
OmniCard/admin/tests/dashboard-workplace.test.ts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

247 行
10 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'path';
import { fileURLToPath } from 'node:url';
const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const pagePath = path.join(adminRoot, 'src/pages/dashboard/workplace/index.tsx');
const localePath = path.join(adminRoot, 'src/pages/dashboard/workplace/locale/index.ts');
const routesPath = path.join(adminRoot, 'src/routes.ts');
const EMOJI_RE =
/[\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
test('1. routes.ts 必须在 dashboard 下注册 workplace 且未被 ignore', () => {
const routesContent = fs.readFileSync(routesPath, 'utf-8');
assert.ok(
routesContent.includes("key: 'dashboard/workplace'"),
'必须注册 dashboard/workplace'
);
assert.ok(
!/key:\s*'dashboard\/workplace'[\s\S]{0,80}ignore:\s*true/.test(routesContent),
'工作台必须在侧边栏展示'
);
});
test('2. 工作台必须对接商务概览 API,并使用 PRD 指定的 Arco 组件', () => {
assert.ok(fs.existsSync(pagePath), '必须存在 workplace/index.tsx');
const content = fs.readFileSync(pagePath, 'utf-8');
assert.ok(
content.includes('/api/admin/crm/radar/workplace'),
'必须调用 /api/admin/crm/radar/workplace'
);
assert.ok(content.includes('@arco-design/web-react'), '必须使用 Arco Design');
assert.ok(content.includes('Statistic'), '必须使用 Statistic 展示 KPI');
assert.ok(content.includes('Timeline'), '必须使用 Timeline 展示雷达动态');
assert.ok(content.includes('Card'), '必须使用 Card');
assert.ok(content.includes('Grid') || content.includes('Row'), '必须使用 Grid');
assert.ok(content.includes('Button'), '必须使用 Button');
assert.ok(!content.includes("from './mock'"), '禁止继续引用 Arco Pro Demo mock');
assert.ok(!content.includes('PopularContents'), '禁止继续引用内容运营 Demo 组件');
});
test('3. 快捷入口必须覆盖新建名片、发卡、线索导出,且仅 1 个主 CTA', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
assert.ok(content.includes('/card/list'), '新建名片必须跳转名片资产库');
assert.ok(content.includes('/nfc/batch'), '发卡必须跳转批量制卡');
assert.ok(
content.includes('/api/admin/crm/lead/page'),
'线索导出必须请求线索分页接口'
);
const primaryButtons = content.match(/type=["']primary["']/g) || [];
assert.equal(primaryButtons.length, 1, '一屏只允许 1 个主 CTA');
});
test('4. 必须引用设计 Token,且零 any、零 emoji', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
assert.ok(
content.includes('@/style/tokens') || content.includes('tokens'),
'必须引用设计 Token'
);
assert.ok(!/\bany\b/.test(content), '严禁 any');
assert.ok(!EMOJI_RE.test(content), '严禁 emoji');
});
test('5. 工作台 i18n 必须覆盖 KPI、雷达动态与快捷入口,且含中英阿', () => {
assert.ok(fs.existsSync(localePath), '必须存在 workplace locale');
const localeContent = fs.readFileSync(localePath, 'utf-8');
assert.ok(localeContent.includes("'en-US'"));
assert.ok(localeContent.includes("'zh-CN'"));
assert.ok(localeContent.includes("'ar-SA'"));
assert.ok(localeContent.includes('workplace.kpi.taps'));
assert.ok(localeContent.includes('workplace.kpi.views'));
assert.ok(localeContent.includes('workplace.kpi.leads'));
assert.ok(localeContent.includes('workplace.kpi.conversion'));
assert.ok(localeContent.includes('workplace.radar.title'));
assert.ok(localeContent.includes('workplace.cta.newCard'));
assert.ok(localeContent.includes('workplace.shortcut.issueCard'));
assert.ok(localeContent.includes('workplace.shortcut.exportLeads'));
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
});
test('6. 专业运营官视角:包含全球雷达监控、渠道漏斗与效率工具,全屏仅 1 个主 CTA', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
const localeContent = fs.readFileSync(localePath, 'utf-8');
// 1. 全球雷达大屏导航
assert.ok(
content.includes('/dashboard/radar-map'),
'必须包含全球雷达热力图入口'
);
// 2. 运营词条在中英阿三语完整定义
const requiredKeys = [
'workplace.radar.liveBadge',
'workplace.funnel.title',
'workplace.channel.title',
'workplace.shortcut.radarMap',
'workplace.shortcut.aiConfig',
];
for (const key of requiredKeys) {
assert.ok(
localeContent.includes(key),
`语言包必须包含运营官核心词条: ${key}`
);
}
// 3. 铁律:无论如何扩展,全屏主 CTA 数量必须严格为 1
const primaryButtons = content.match(/type=["']primary["']/g) || [];
assert.equal(
primaryButtons.length,
1,
'扩展后全屏依然必须严格保持唯 1 一个主 CTA (type="primary")'
);
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
});
test('7. 全球运营快捷中枢:覆盖8大核心业务快捷入口与运营预警,全屏仅1个主CTA', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
const localeContent = fs.readFileSync(localePath, 'utf-8');
// 必须覆盖的8大核心业务快捷路由
const requiredRoutes = [
'/card/builder',
'/crm/leads',
'/crm/pipeline',
'/nfc/batch',
'/nfc/devices',
'/dashboard/radar-map',
'/ai/concierge',
'/dashboard/financial',
];
for (const route of requiredRoutes) {
assert.ok(content.includes(route), `工作台必须包含快捷业务路由入口: ${route}`);
}
// 必须包含待办预警词条
assert.ok(localeContent.includes('workplace.alerts.title'), '必须包含待办预警标题');
assert.ok(localeContent.includes('workplace.quickHub.title'), '必须包含快捷中枢标题');
// 铁律:全屏主 CTA 依然严格为 1
const primaryButtons = content.match(/type=["']primary["']/g) || [];
assert.equal(
primaryButtons.length,
1,
'无论增加多少快捷入口,全屏依然必须严格保持唯 1 一个主 CTA (type="primary")'
);
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
});
test('8. 实时雷达数据专家重构:相对时间、本地环境优雅降级、名片归属与科学意向分分级', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
const localeContent = fs.readFileSync(localePath, 'utf-8');
// 1. 相对时间与本地化支持
const radarKeys = [
'workplace.radar.localEnv',
'workplace.radar.justNow',
'workplace.radar.minsAgo',
'workplace.radar.hoursAgo',
'workplace.radar.bounce',
'workplace.radar.cardFallback',
];
for (const key of radarKeys) {
assert.ok(
localeContent.includes(key),
`语言包必须包含雷达数据语义化词条: ${key}`
);
}
// 2. 页面中实现相对时间格式化与本地环境优雅降级
assert.ok(content.includes('formatRelativeTime'), '必须包含相对时间计算逻辑');
assert.ok(content.includes('formatLocation'), '必须包含本地环境优雅降级逻辑');
assert.ok(content.includes('formatStayText'), '必须包含跳出/停留文案格式化');
assert.ok(content.includes('getIntentTagColor'), '必须包含科学意向分语义色彩分级');
// 3. 规范约束
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
assert.ok(!EMOJI_RE.test(content), '页面代码严禁 emoji');
assert.ok(!/\bany\b/.test(content), '严禁使用 any');
});
test('9. P32: KPI 必须来自 workplace 统计接口,禁止演示假数,失败回退 0/空态', () => {
const content = fs.readFileSync(pagePath, 'utf-8');
const localeContent = fs.readFileSync(localePath, 'utf-8');
assert.ok(
content.includes('/api/admin/crm/radar/workplace'),
'KPI 必须请求 /api/admin/crm/radar/workplace'
);
assert.ok(content.includes('kpis.tapCount'), '触碰量必须绑定接口 kpis.tapCount');
assert.ok(content.includes('kpis.viewCount'), '浏览量必须绑定接口 kpis.viewCount');
assert.ok(content.includes('kpis.newLeadCount'), '新线索必须绑定接口 kpis.newLeadCount');
assert.ok(
content.includes('kpis.conversionRate'),
'转化率必须绑定接口 kpis.conversionRate'
);
// 禁止渠道/KPI 演示假数(空 feed 时不得回退 50/30/20)
assert.ok(
!/nfc:\s*50/.test(content) && !/qr:\s*30/.test(content) && !/link:\s*20/.test(content),
'渠道分布禁止硬编码 50/30/20 假数'
);
assert.ok(!content.includes('14500'), '禁止硬编码演示 KPI 数字');
assert.ok(!content.includes('Vanguard'), '禁止硬编码演示客户名');
assert.ok(!/\b210\b/.test(content), '页面禁止硬编码演示停留秒数');
assert.ok(
!localeContent.includes('210'),
'语言包禁止硬编码演示停留秒数假数'
);
assert.ok(
content.includes('code !== 1000') || content.includes('code === 1000'),
'必须按 Cool-Admin 约定校验 code === 1000'
);
// 失败或无数据时必须回退 EMPTY_KPI / 全 0,不得保留假数
assert.ok(content.includes('EMPTY_KPI'), '必须定义 EMPTY_KPI 作为空态/失败回退');
assert.ok(
content.includes('kpis: EMPTY_KPI, radarFeed: []'),
'接口失败或无效响应时必须 setOverview 回退 EMPTY_KPI'
);
// 漏斗 meta 与渠道/预警空态必须走 locale
const localeKeys = [
'workplace.funnel.metaBase',
'workplace.funnel.metaTapView',
'workplace.funnel.metaViewInteract',
'workplace.funnel.metaInteractLead',
'workplace.channel.empty',
'workplace.alerts.empty',
];
for (const key of localeKeys) {
assert.ok(localeContent.includes(key), `语言包必须包含: ${key}`);
assert.ok(content.includes(key), `页面必须引用 locale key: ${key}`);
}
assert.ok(!EMOJI_RE.test(content), '页面严禁 emoji');
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
assert.ok(!/\bany\b/.test(content), '严禁 any');
});