补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
146 行
5.2 KiB
TypeScript
146 行
5.2 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';
|
||
|
||
const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const pagePath = path.join(adminRoot, 'src/pages/platform/subscribers/index.tsx');
|
||
const localePath = path.join(adminRoot, 'src/pages/platform/subscribers/locale/index.ts');
|
||
|
||
const EMOJI_RE = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
|
||
|
||
const HARDCODED_SUCCESS = [
|
||
'会员配置已更新',
|
||
'延长有效期',
|
||
'会员状态已切换',
|
||
];
|
||
|
||
function readPage(): string {
|
||
assert.ok(fs.existsSync(pagePath), '必须存在 platform/subscribers/index.tsx');
|
||
return fs.readFileSync(pagePath, 'utf-8');
|
||
}
|
||
|
||
function readLocale(): string {
|
||
assert.ok(fs.existsSync(localePath), '必须存在 platform/subscribers/locale/index.ts');
|
||
return fs.readFileSync(localePath, 'utf-8');
|
||
}
|
||
|
||
test('1. R20: 必须有页面 locale,且含成功/失败词条与三语', () => {
|
||
const localeContent = readLocale();
|
||
const keys = [
|
||
'platformSubscribers.update.success',
|
||
'platformSubscribers.update.fail',
|
||
'platformSubscribers.extend.success',
|
||
'platformSubscribers.extend.fail',
|
||
'platformSubscribers.status.success',
|
||
'platformSubscribers.status.fail',
|
||
];
|
||
for (const key of keys) {
|
||
assert.ok(localeContent.includes(key), `locale 必须包含: ${key}`);
|
||
}
|
||
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');
|
||
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
|
||
assert.ok(!/\bany\b/.test(localeContent), 'locale 禁止 any');
|
||
});
|
||
|
||
test('2. R20: 页面必须引入 locale,成功文案走 t[...]', () => {
|
||
const content = readPage();
|
||
assert.ok(
|
||
content.includes("from './locale'") || content.includes('from "./locale"'),
|
||
'必须引入页面 locale'
|
||
);
|
||
assert.ok(content.includes('useLocale(locale)'), '必须 useLocale(locale)');
|
||
|
||
const successKeys = [
|
||
'platformSubscribers.update.success',
|
||
'platformSubscribers.extend.success',
|
||
'platformSubscribers.status.success',
|
||
];
|
||
for (const key of successKeys) {
|
||
assert.ok(content.includes(key), `页面必须引用成功 locale key: ${key}`);
|
||
}
|
||
|
||
const failKeys = [
|
||
'platformSubscribers.update.fail',
|
||
'platformSubscribers.extend.fail',
|
||
'platformSubscribers.status.fail',
|
||
];
|
||
for (const key of failKeys) {
|
||
assert.ok(content.includes(key), `页面必须引用失败 locale key: ${key}`);
|
||
}
|
||
});
|
||
|
||
test('3. R20: 延期/改配置/切换状态仅在 code===1000 时 Message.success', () => {
|
||
const content = readPage();
|
||
|
||
assert.ok(
|
||
content.includes('/api/admin/team/platform/subscribers/update'),
|
||
'操作必须走 subscribers/update'
|
||
);
|
||
|
||
const successMatches = [...content.matchAll(/Message\.success\s*\(/g)];
|
||
assert.ok(successMatches.length >= 3, '改配置/延期/切状态成功时仍应 Message.success');
|
||
for (const match of successMatches) {
|
||
const index = match.index ?? 0;
|
||
const preceding = content.slice(Math.max(0, index - 280), index);
|
||
assert.ok(
|
||
/code\s*===\s*1000/.test(preceding),
|
||
`Message.success 必须位于 code === 1000 分支内,附近片段: ${preceding.slice(-100)}`
|
||
);
|
||
}
|
||
});
|
||
|
||
test('4. R20: 失败不得报延期成功或状态已切换;catch/非1000 不得 Message.success', () => {
|
||
const content = readPage();
|
||
|
||
assert.ok(
|
||
content.includes('code !== 1000') || content.includes('code === 1000'),
|
||
'必须按 Cool-Admin code 校验'
|
||
);
|
||
|
||
// 硬编码中文成功句不得再出现在页面源码(尤其 Message.success)
|
||
for (const phrase of HARDCODED_SUCCESS) {
|
||
assert.ok(
|
||
!content.includes(phrase),
|
||
`页面禁止硬编码成功句: ${phrase}`
|
||
);
|
||
}
|
||
|
||
const failBlocks = content.match(
|
||
/else\s*\{[\s\S]{0,220}\}|catch\s*(?:\([^)]*\))?\s*\{[\s\S]{0,220}\}/g
|
||
);
|
||
if (failBlocks) {
|
||
for (const block of failBlocks) {
|
||
assert.ok(
|
||
!block.includes('Message.success'),
|
||
`失败分支内不得 Message.success: ${block.slice(0, 120)}`
|
||
);
|
||
}
|
||
}
|
||
|
||
// 非 1000 分支须 Message.error(正分支 return 后、或 !== 1000 早退)
|
||
assert.ok(
|
||
/code\s*!==\s*1000[\s\S]{0,160}Message\.error/.test(content) ||
|
||
/if\s*\(\s*res\.data\?\.code\s*!==\s*1000[\s\S]{0,200}Message\.error/.test(
|
||
content
|
||
) ||
|
||
(/code\s*===\s*1000[\s\S]{0,400}Message\.error/.test(content) &&
|
||
content.includes('Message.error')),
|
||
'code 非 1000 时必须 Message.error'
|
||
);
|
||
assert.ok(content.includes('Message.error'), '失败须 Message.error');
|
||
});
|
||
|
||
test('5. R20: 禁止造假租户;操作不写演示企业名', () => {
|
||
const content = readPage();
|
||
assert.ok(!content.includes('Vanguard'), '禁止硬编码演示租户名 Vanguard');
|
||
assert.ok(!content.includes('Acme Corp'), '禁止硬编码演示租户名');
|
||
assert.ok(!content.includes('演示企业'), '禁止硬编码演示企业');
|
||
assert.ok(!content.includes('Mock.js') && !content.includes("from 'mockjs'"), '禁止 Mock.js');
|
||
assert.ok(!/\bany\b/.test(content), '严禁 any');
|
||
assert.ok(!EMOJI_RE.test(content), '页面严禁 emoji');
|
||
});
|