补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
203 行
6.9 KiB
TypeScript
203 行
6.9 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 defaultI18n from '../src/locale/index.ts';
|
||
|
||
const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const auditPagePath = path.join(adminRoot, 'src/pages/settings/audit-logs/index.tsx');
|
||
const auditLocalePath = path.join(
|
||
adminRoot,
|
||
'src/pages/settings/audit-logs/locale/index.ts'
|
||
);
|
||
const routesPath = path.join(adminRoot, 'src/routes.ts');
|
||
|
||
function readPage(): string {
|
||
assert.ok(fs.existsSync(auditPagePath), '安全审计日志页面文件必须存在');
|
||
return fs.readFileSync(auditPagePath, 'utf-8');
|
||
}
|
||
|
||
test('1. 路由配置中必须包含 settings/audit-logs 并在主语言包中声明菜单词条', () => {
|
||
assert.ok(fs.existsSync(routesPath), 'routes.ts 必须存在');
|
||
const routesContent = fs.readFileSync(routesPath, 'utf-8');
|
||
|
||
assert.ok(
|
||
routesContent.includes("'settings/audit-logs'") ||
|
||
routesContent.includes('"settings/audit-logs"'),
|
||
'routes.ts 必须注册 settings/audit-logs'
|
||
);
|
||
assert.ok(
|
||
routesContent.includes('menu.settings.auditLogs'),
|
||
'routes.ts 必须使用 menu.settings.auditLogs 作为国际化 key'
|
||
);
|
||
|
||
assert.equal(
|
||
defaultI18n['en-US']['menu.settings.auditLogs'],
|
||
'Audit Logs',
|
||
'en-US 主词典必须包含 menu.settings.auditLogs'
|
||
);
|
||
assert.ok(
|
||
defaultI18n['zh-CN']['menu.settings.auditLogs'] === '安全审计' ||
|
||
defaultI18n['zh-CN']['menu.settings.auditLogs'] === '审计日志',
|
||
'zh-CN 主词典必须包含 menu.settings.auditLogs'
|
||
);
|
||
});
|
||
|
||
test('2. settings/audit-logs 页面必须采用 Arco Table, DatePicker, Select, Tag, Input 等官方组件', () => {
|
||
const content = readPage();
|
||
|
||
assert.ok(content.includes('Table'), '必须使用 Arco Table');
|
||
assert.ok(content.includes('DatePicker'), '必须使用 Arco DatePicker');
|
||
assert.ok(content.includes('Select'), '必须使用 Arco Select');
|
||
assert.ok(content.includes('Tag'), '必须使用 Arco Tag');
|
||
assert.ok(content.includes('Input'), '必须使用 Arco Input/Input.Search');
|
||
assert.ok(
|
||
content.includes("from '@arco-design/web-react'"),
|
||
'必须从 @arco-design/web-react 引入组件'
|
||
);
|
||
assert.ok(
|
||
content.includes('admin/base/audit/page') ||
|
||
content.includes('/api/admin/base/audit/page'),
|
||
'必须请求 POST /api/admin/base/audit/page'
|
||
);
|
||
assert.ok(
|
||
content.includes('code === 1000') || content.includes('code ===1000'),
|
||
'必须按 Cool-Admin code === 1000 判断成功'
|
||
);
|
||
});
|
||
|
||
test('3. 严格遵循瑞士国际主义风格、单一主 CTA、引用设计 Token,且零 any、零 emoji', () => {
|
||
const content = readPage();
|
||
|
||
// 单一主 CTA: 统计 type="primary"
|
||
const primaryButtons = content.match(/type=["']primary["']/g) || [];
|
||
assert.equal(
|
||
primaryButtons.length,
|
||
1,
|
||
'页面必须严格保持单一主 CTA (仅有 1 个 type="primary")'
|
||
);
|
||
|
||
// 严格 Token 引用,禁止硬编码颜色与纯黑
|
||
assert.ok(content.includes('lightColors'), '必须引用 lightColors token');
|
||
assert.ok(
|
||
!/#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})\b/.test(content),
|
||
'禁止硬编码 HEX 颜色值'
|
||
);
|
||
assert.ok(!content.includes('#000'), '禁止纯黑 #000');
|
||
|
||
// 零 any
|
||
assert.ok(!/\bany\b/.test(content), '页面严禁使用 any');
|
||
|
||
// 零 emoji
|
||
assert.ok(
|
||
!/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u.test(content),
|
||
'页面严禁包含 emoji'
|
||
);
|
||
});
|
||
|
||
test('4. settings/audit-logs 语言包必须包含中英文案且零 emoji、零 any', () => {
|
||
assert.ok(fs.existsSync(auditLocalePath), '审计日志语言包文件必须存在');
|
||
const localeContent = fs.readFileSync(auditLocalePath, 'utf-8');
|
||
|
||
assert.ok(localeContent.includes("'en-US'"), '必须包含英文词典');
|
||
assert.ok(localeContent.includes("'zh-CN'"), '必须包含中文词典');
|
||
assert.ok(
|
||
localeContent.includes('menu.settings.auditLogs'),
|
||
'必须声明 menu.settings.auditLogs'
|
||
);
|
||
assert.ok(!/\bany\b/.test(localeContent), '语言包严禁使用 any');
|
||
assert.ok(
|
||
!/[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u.test(
|
||
localeContent
|
||
),
|
||
'语言包严禁包含 emoji'
|
||
);
|
||
});
|
||
|
||
test('5. R18: 必须支持按 action 筛选,并将所选动作作为 page 请求参数发出', () => {
|
||
const content = readPage();
|
||
const localeContent = fs.readFileSync(auditLocalePath, 'utf-8');
|
||
|
||
assert.ok(
|
||
content.includes('selectedAction'),
|
||
'必须具备 selectedAction 筛选状态'
|
||
);
|
||
assert.ok(
|
||
content.includes('ACTION_OPTIONS') ||
|
||
(content.includes('nfc_freeze') && content.includes('lead_batch_export')),
|
||
'必须提供可选动作列表'
|
||
);
|
||
assert.ok(
|
||
/if\s*\(\s*filters\.action\s*\)\s*\{[\s\S]*?payload\.action\s*=\s*filters\.action/.test(
|
||
content
|
||
),
|
||
'仅当已选动作时写入 payload.action;清空筛选时不带空动作'
|
||
);
|
||
assert.ok(
|
||
!/payload\.action\s*=\s*['"]['"]/.test(content),
|
||
'禁止把空字符串硬编码进 payload.action'
|
||
);
|
||
assert.ok(
|
||
content.includes('setSelectedAction'),
|
||
'重置/变更筛选必须能更新 selectedAction'
|
||
);
|
||
assert.ok(
|
||
localeContent.includes("'auditLogs.filter.action'"),
|
||
'locale 必须包含 auditLogs.filter.action'
|
||
);
|
||
assert.ok(
|
||
localeContent.includes('auditLogs.actionType.nfc_freeze') &&
|
||
localeContent.includes('auditLogs.actionType.lead_batch_export'),
|
||
'locale 必须覆盖动作选项文案'
|
||
);
|
||
assert.ok(
|
||
content.includes("t['auditLogs.filter.action']"),
|
||
'action 筛选 placeholder 必须走 locale'
|
||
);
|
||
});
|
||
|
||
test('6. R18: 请求失败或 code 非 1000 必须清空表格,禁止残留示例日志', () => {
|
||
const content = readPage();
|
||
|
||
assert.ok(
|
||
/useState\s*<\s*BaseAuditLog\s*\[\s*\]\s*>\s*\(\s*\[\s*\]\s*\)/.test(
|
||
content
|
||
),
|
||
'初始 dataList 必须为空数组'
|
||
);
|
||
// else(非 1000)与 catch 两处都必须清空
|
||
const clearCalls = content.match(/setDataList\(\[\]\)/g) || [];
|
||
assert.ok(
|
||
clearCalls.length >= 2,
|
||
'code 非 1000 与 catch 均须 setDataList([]) 清空表格'
|
||
);
|
||
assert.ok(
|
||
content.includes('setTotal(0)'),
|
||
'失败时 total 也须归零'
|
||
);
|
||
assert.ok(
|
||
content.includes("t['auditLogs.request.fail']"),
|
||
'失败提示必须走 locale,禁止硬编码'
|
||
);
|
||
assert.ok(
|
||
!content.includes('Mock.js') &&
|
||
!content.includes('mockList') &&
|
||
!content.includes('示例日志') &&
|
||
!content.includes('sampleLogs') &&
|
||
!content.includes('fakeLogs'),
|
||
'禁止写死示例日志行或 mock 列表'
|
||
);
|
||
assert.ok(
|
||
!/operatorName:\s*['"][^'"]+['"]/.test(content) &&
|
||
!/detail:\s*['"][^'"]+['"]/.test(content) &&
|
||
!/ip:\s*['"]\d+\.\d+\.\d+\.\d+['"]/.test(content),
|
||
'禁止内联写死审计日志行字段'
|
||
);
|
||
// 成功分支才写入 list;失败不得回退到本地示例数组
|
||
assert.ok(
|
||
/res\.data\.list/.test(content) || /data\.list/.test(content),
|
||
'表格数据必须来自接口 list'
|
||
);
|
||
});
|