文件
OmniCard/admin/tests/crm-radar-log.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

200 行
7.8 KiB
TypeScript

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 radarLogPagePath = path.join(adminRoot, 'src/pages/crm/radar-log/index.tsx');
const radarLogLocalePath = path.join(
adminRoot,
'src/pages/crm/radar-log/locale/index.ts'
);
function flattenRouteKeys(items: typeof routes): string[] {
return items.flatMap((item) => [
item.key,
...(item.children ? flattenRouteKeys(item.children) : []),
]);
}
test('1. routes.ts 必须包含 key crm/radar-log,供 layout glob 加载', () => {
const keys = flattenRouteKeys(routes);
assert.ok(keys.includes('crm'), '应存在一级菜单 key: crm');
assert.ok(keys.includes('crm/radar-log'), '应存在路由 key: crm/radar-log');
const crmGroup = routes.find((item) => item.key === 'crm');
assert.ok(crmGroup, '应存在 menu.crm 分组');
assert.equal(crmGroup?.name, 'menu.crm');
assert.ok(
crmGroup?.children?.some(
(child) =>
child.key === 'crm/radar-log' && child.name === 'menu.crm.radarLog'
),
'crm 分组下应有 menu.crm.radarLog / crm/radar-log'
);
});
test('2. locale 必须包含 menu.crm.radarLog 中英文案', () => {
assert.equal(defaultI18n['en-US']['menu.crm.radarLog'], 'Visitor Logs');
assert.equal(defaultI18n['zh-CN']['menu.crm.radarLog'], '访客足迹');
});
test('3. radar-log/index.tsx 必须请求 admin/crm/radar/page 并使用 Arco Table', () => {
assert.ok(fs.existsSync(radarLogPagePath), '访客足迹页面必须存在');
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(
content.includes('admin/crm/radar/page'),
'必须请求 POST /api/admin/crm/radar/page'
);
assert.ok(content.includes('Table'), '必须使用 Arco Table');
assert.ok(
content.includes("from '@arco-design/web-react'"),
'必须从 @arco-design/web-react 引入组件'
);
assert.ok(content.includes('Form'), '必须使用 Arco Form');
assert.ok(content.includes('Select'), '必须使用 Arco Select');
assert.ok(content.includes('Tag'), '必须使用 Arco Tag');
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');
});
test('4. radar-log/index.tsx 必须按 RadarTrack / TouchType 渲染足迹列与筛选', () => {
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(
content.includes('RadarTrack') && content.includes('TouchType'),
'必须引用 RadarTrack / TouchType'
);
assert.ok(content.includes("from '@/types/omni'"), '必须复用 admin/src/types/omni.ts');
assert.ok(content.includes('touchType'), '必须包含 touchType 筛选与列');
assert.ok(content.includes("'nfc'") && content.includes("'qr'") && content.includes("'link'"), 'touchType 必须覆盖 nfc/qr/link');
assert.ok(content.includes('cardId'), '必须支持 cardId 筛选');
assert.ok(content.includes('country'), '必须支持 country 筛选');
assert.ok(content.includes('keyWord'), '请求体必须包含 keyWord');
assert.ok(
content.includes('createTime') && content.includes('createdAt'),
'必须兼容 createTime / createdAt'
);
assert.ok(content.includes('visitorId'), '必须展示 visitorId');
assert.ok(content.includes('stayDurationMs'), '必须展示 stayDurationMs');
assert.ok(content.includes('scrollDepth'), '必须展示 scrollDepth');
assert.ok(content.includes('viewedBlocks'), '必须展示 viewedBlocks');
assert.ok(content.includes('setData([])'), '失败必须清空表格');
});
test('5. 页面 locale 必须覆盖筛选与列文案,且禁止 any', () => {
assert.ok(fs.existsSync(radarLogLocalePath), '访客足迹 locale 必须存在');
const content = fs.readFileSync(radarLogLocalePath, 'utf-8');
assert.ok(content.includes('radarLog.filter.cardId'), '必须有 cardId 筛选文案');
assert.ok(content.includes('radarLog.filter.touchType'), '必须有 touchType 筛选文案');
assert.ok(content.includes('radarLog.filter.country'), '必须有 country 筛选文案');
assert.ok(content.includes('radarLog.columns.viewedBlocks'), '必须有 viewedBlocks 列文案');
assert.ok(!/\bany\b/.test(content), 'locale 禁止使用 any');
});
test('6. 深度访问高亮:停留 ≥180000ms 或滚动深度 ≥0.6;缺字段不高亮', () => {
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(
content.includes('180000'),
'必须包含停留时长阈值 180000 毫秒'
);
assert.ok(
content.includes('0.6'),
'必须包含滚动深度阈值 0.6'
);
assert.ok(
/isDeepVisit|deepVisit/.test(content),
'必须具备 isDeepVisit / deepVisit 判定'
);
assert.ok(
content.includes('rowClassName') || content.includes('onRow'),
'必须通过 rowClassName 或 onRow 对深度访问行高亮'
);
// 缺字段:须对 stayDurationMs / scrollDepth 做有限数值校验后再比阈值
assert.ok(
/Number\.isFinite|typeof[\s\S]{0,40}number/.test(content),
'字段缺失或非有限数时不得误判为深度访问'
);
assert.ok(
content.includes("t['radarLog.deepVisit']") ||
content.includes('radarLog.deepVisit'),
'深度访问文案必须走 locale'
);
});
test('7. locale 必须含深度访问文案;页面禁止写死国家或示例访客', () => {
const pageContent = fs.readFileSync(radarLogPagePath, 'utf-8');
const localeContent = fs.readFileSync(radarLogLocalePath, 'utf-8');
assert.ok(
localeContent.includes("'radarLog.deepVisit'"),
'locale 必须包含 radarLog.deepVisit'
);
assert.ok(localeContent.includes("'en-US'"), 'locale 必须含 en-US');
assert.ok(localeContent.includes("'zh-CN'"), 'locale 必须含 zh-CN');
assert.ok(localeContent.includes("'ar-SA'"), 'locale 必须含 ar-SA');
const bannedCountries = [
'United States',
'United Kingdom',
'Singapore',
'Germany',
'Japan',
'China',
'Dubai',
'Saudi Arabia',
];
for (const name of bannedCountries) {
assert.ok(!pageContent.includes(name), `禁止硬编码国家/地区名: ${name}`);
}
assert.ok(
!pageContent.includes('示例访客') &&
!pageContent.includes('sample-visitor') &&
!pageContent.includes('visitor_demo') &&
!pageContent.includes('Alex') &&
!/visitorId\s*:\s*['"][^'"]+['"]/.test(pageContent),
'禁止写死示例访客行'
);
assert.ok(
!/useState\s*<\s*RadarLogRow\s*\[\s*\]\s*>\s*\(\s*\[[^\]]+visitorId/.test(
pageContent
) &&
!/const\s+MOCK_|const\s+SAMPLE_|const\s+DEMO_/.test(pageContent),
'禁止用示例访客初始化表格数据'
);
});
test('8. 请求失败必须空表,不得残留示例访客', () => {
const content = fs.readFileSync(radarLogPagePath, 'utf-8');
assert.ok(content.includes('setData([])'), '失败必须 setData([])');
assert.ok(
/useState[<\s(][^)]*RadarLogRow[^)]*\)?\s*\(\s*\[\s*\]\s*\)/.test(content) ||
/useState\s*<\s*RadarLogRow\s*\[\s*\]\s*>\s*\(\s*\[\s*\]\s*\)/.test(content),
'初始 data 必须为空数组'
);
// 非 1000 与 catch 均应清空
assert.ok(
/code\s*===\s*1000[\s\S]*?setData\(\[\]\)/.test(content) ||
(content.includes('setData([])') && content.includes('catch')),
'code 非 1000 或 catch 必须清空表格'
);
assert.ok(
!content.includes('Mock.js') && !content.includes('mockList'),
'失败路径禁止留下 mock/示例列表'
);
});