补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
200 行
7.8 KiB
TypeScript
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/示例列表'
|
|
);
|
|
});
|