文件
OmniCard/admin/tests/settings-audit-logs.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

203 行
6.9 KiB
TypeScript

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

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

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'
);
});