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