补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
206 行
7.5 KiB
TypeScript
206 行
7.5 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/subscriptions/index.tsx'
|
|
);
|
|
const localePath = path.join(
|
|
adminRoot,
|
|
'src/pages/platform/subscriptions/locale/index.ts'
|
|
);
|
|
|
|
const EMOJI_RE = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
|
|
const HARDCODED_SUCCESS_RE = /规格配置已更新|配置已更新/;
|
|
const HARDCODED_PRICE_RE =
|
|
/monthlyPrice\s*[:=]\s*\d+|yearlyPrice\s*[:=]\s*\d+|\$\s*(9|19|29|49|99|199|299)\b/;
|
|
|
|
function readPage(): string {
|
|
assert.ok(fs.existsSync(pagePath), '必须存在 platform/subscriptions/index.tsx');
|
|
return fs.readFileSync(pagePath, 'utf-8');
|
|
}
|
|
|
|
function readLocale(): string {
|
|
assert.ok(
|
|
fs.existsSync(localePath),
|
|
'必须存在 platform/subscriptions/locale/index.ts'
|
|
);
|
|
return fs.readFileSync(localePath, 'utf-8');
|
|
}
|
|
|
|
test('1. R19: 订阅规格页必须请求 plans list/update,并按 code===1000 才成功提示', () => {
|
|
const content = readPage();
|
|
|
|
assert.ok(
|
|
content.includes('/api/admin/team/platform/plans/list'),
|
|
'必须请求 plans/list'
|
|
);
|
|
assert.ok(
|
|
content.includes('/api/admin/team/platform/plans/update'),
|
|
'必须请求 plans/update'
|
|
);
|
|
assert.ok(content.includes('axios'), '必须使用 axios');
|
|
assert.ok(
|
|
content.includes('code === 1000') || content.includes('code ===1000'),
|
|
'必须按 Cool-Admin code === 1000 判断保存成功'
|
|
);
|
|
assert.ok(content.includes('Message.success'), '真实保存成功仍应 Message.success');
|
|
assert.ok(content.includes('Message.error'), '失败必须 Message.error');
|
|
assert.ok(content.includes('PlanConfigItem'), '必须使用 PlanConfigItem 类型');
|
|
assert.ok(content.includes("from '@/types/omni'"), '必须复用 omni 类型');
|
|
assert.ok(!/\bany\b/.test(content), '严禁 any');
|
|
assert.ok(
|
|
!content.includes('Mock.js') && !content.includes("from 'mockjs'"),
|
|
'禁止 Mock.js'
|
|
);
|
|
});
|
|
|
|
test('2. R19: Message.success 仅允许出现在 code===1000 分支,失败不得报已更新', () => {
|
|
const content = readPage();
|
|
|
|
const successMatches = [...content.matchAll(/Message\.success\s*\(/g)];
|
|
assert.ok(successMatches.length > 0, '必须保留保存成功 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(-80)}`
|
|
);
|
|
}
|
|
|
|
const failBlocks = content.match(
|
|
/else\s*\{[\s\S]{0,200}\}|catch\s*(?:\([^)]*\))?\s*\{[\s\S]{0,200}\}/g
|
|
);
|
|
if (failBlocks) {
|
|
for (const block of failBlocks) {
|
|
assert.ok(
|
|
!block.includes('Message.success'),
|
|
`失败分支内不得 Message.success: ${block.slice(0, 120)}`
|
|
);
|
|
assert.ok(
|
|
!HARDCODED_SUCCESS_RE.test(block),
|
|
'失败分支不得硬编码已更新文案'
|
|
);
|
|
}
|
|
}
|
|
|
|
assert.ok(
|
|
content.includes("t['platformSubscriptions.request.updateSuccess']") ||
|
|
content.includes('t["platformSubscriptions.request.updateSuccess"]'),
|
|
'成功提示必须走 locale updateSuccess'
|
|
);
|
|
assert.ok(
|
|
content.includes("t['platformSubscriptions.request.saveFail']") ||
|
|
content.includes('t["platformSubscriptions.request.saveFail"]') ||
|
|
content.includes("t['platformSubscriptions.request.updateFail']") ||
|
|
content.includes('t["platformSubscriptions.request.updateFail"]'),
|
|
'保存失败必须走 locale 失败文案'
|
|
);
|
|
assert.ok(
|
|
content.includes("t['platformSubscriptions.request.fail']") ||
|
|
content.includes('t["platformSubscriptions.request.fail"]'),
|
|
'加载失败必须走 locale request.fail'
|
|
);
|
|
});
|
|
|
|
test('3. R19: 成功/失败文案走 locale,禁止硬编码成功句与套餐价格', () => {
|
|
const content = readPage();
|
|
const localeContent = readLocale();
|
|
|
|
assert.ok(
|
|
content.includes("from './locale'") || content.includes('from "./locale"'),
|
|
'必须引入页面 locale'
|
|
);
|
|
assert.ok(content.includes('useLocale(locale)'), '必须 useLocale(locale)');
|
|
|
|
assert.ok(!HARDCODED_SUCCESS_RE.test(content), '页面禁止硬编码「规格配置已更新」');
|
|
assert.ok(
|
|
!HARDCODED_SUCCESS_RE.test(localeContent) ||
|
|
localeContent.includes('platformSubscriptions.request.updateSuccess'),
|
|
'成功句只能作为 locale 词条值,不得散落在脚本模板'
|
|
);
|
|
// 页面脚本不得把成功句写死在模板字符串里
|
|
assert.ok(
|
|
!/`[^`]*规格配置已更新[^`]*`/.test(content) &&
|
|
!/'[^']*规格配置已更新[^']*'/.test(content) &&
|
|
!/"[^"]*规格配置已更新[^"]*"/.test(content),
|
|
'页面不得硬编码规格配置已更新'
|
|
);
|
|
assert.ok(!content.includes('获取套餐配置失败'), '加载失败不得硬编码中文');
|
|
|
|
const localeKeys = [
|
|
'menu.platform.subscriptions',
|
|
'platformSubscriptions.request.fail',
|
|
'platformSubscriptions.request.updateSuccess',
|
|
'platformSubscriptions.request.saveFail',
|
|
];
|
|
for (const key of localeKeys) {
|
|
assert.ok(localeContent.includes(key), `语言包必须包含: ${key}`);
|
|
}
|
|
assert.ok(
|
|
content.includes('platformSubscriptions.request.updateSuccess'),
|
|
'页面必须引用 updateSuccess'
|
|
);
|
|
assert.ok(
|
|
content.includes('platformSubscriptions.request.saveFail') ||
|
|
content.includes('platformSubscriptions.request.updateFail'),
|
|
'页面必须引用保存失败 key'
|
|
);
|
|
assert.ok(
|
|
content.includes('platformSubscriptions.request.fail'),
|
|
'页面必须引用 request.fail'
|
|
);
|
|
|
|
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');
|
|
|
|
// 价格来自接口 plan.monthlyPrice / yearlyPrice,禁止写死套餐价
|
|
assert.ok(
|
|
content.includes('plan.monthlyPrice') || content.includes('monthlyPrice'),
|
|
'月价必须绑定接口字段'
|
|
);
|
|
assert.ok(
|
|
content.includes('plan.yearlyPrice') || content.includes('yearlyPrice'),
|
|
'年价必须绑定接口字段'
|
|
);
|
|
assert.ok(!HARDCODED_PRICE_RE.test(content), '页面禁止写死套餐价格');
|
|
assert.ok(!HARDCODED_PRICE_RE.test(localeContent), 'locale 禁止写死套餐价格');
|
|
|
|
assert.ok(!EMOJI_RE.test(content), '页面严禁 emoji');
|
|
assert.ok(!EMOJI_RE.test(localeContent), '语言包严禁 emoji');
|
|
assert.ok(!/\bany\b/.test(localeContent), 'locale 严禁 any');
|
|
|
|
// 自审:模板/脚本不得把成功句写死;Message.success 只能引用 locale key
|
|
const successCalls = [
|
|
...content.matchAll(/Message\.success\s*\(\s*([^)]+)\s*\)/g),
|
|
];
|
|
assert.ok(successCalls.length > 0, '必须有 Message.success 调用');
|
|
for (const match of successCalls) {
|
|
const arg = match[1] ?? '';
|
|
assert.ok(
|
|
/platformSubscriptions\.request\.updateSuccess/.test(arg),
|
|
`Message.success 参数必须引用 updateSuccess locale key,实际: ${arg}`
|
|
);
|
|
assert.ok(
|
|
!HARDCODED_SUCCESS_RE.test(arg),
|
|
'Message.success 参数不得内联硬编码成功句'
|
|
);
|
|
}
|
|
assert.ok(
|
|
!content.includes('规格配置已更新'),
|
|
'页面脚本/模板自审:不得出现硬编码成功句「规格配置已更新」'
|
|
);
|
|
assert.ok(
|
|
/'platformSubscriptions\.request\.updateSuccess'\s*:\s*'/.test(
|
|
localeContent
|
|
),
|
|
'成功句只能存在于 locale 词条值'
|
|
);
|
|
});
|