补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
132 行
6.2 KiB
TypeScript
132 行
6.2 KiB
TypeScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
|
||
const casesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardCases.uts');
|
||
const casesPagePath = path.join(MP_ROOT, 'pages/owner/card/cases.uvue');
|
||
|
||
/** 禁止预填的示例案例 / 客户 / 假 KPI */
|
||
const SAMPLE_BAN_PATTERNS: RegExp[] = [
|
||
/全球智能硬件数字化交付方案/,
|
||
/阿联酋国际商业联合会/,
|
||
/触达\s*10,000\+?\s*买家/,
|
||
/\+320%\s*ROI/i,
|
||
/\+150%\s*留资/,
|
||
/45k\+?\s*国际线索/i,
|
||
/2M\+?\s*全球曝光/i,
|
||
/Top\s*1\s*细分类目/i,
|
||
/VoltRide/i,
|
||
];
|
||
|
||
test('1. useCardCases.uts 文件必须存在且格式完整', () => {
|
||
assert.ok(fs.existsSync(casesUtsPath), '必须存在 useCardCases.uts');
|
||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
assert.ok(content.includes('export function createCasesState'), '必须导出 createCasesState');
|
||
assert.ok(content.includes('export function validateCaseForm'), '必须导出 validateCaseForm');
|
||
assert.ok(content.includes('export async function loadCases'), '必须导出 loadCases');
|
||
assert.ok(content.includes('export async function saveCase'), '必须导出 saveCase');
|
||
assert.ok(content.includes('export async function deleteCase'), '必须导出 deleteCase');
|
||
assert.ok(content.includes('export function resolveCasesText'), '必须导出 resolveCasesText');
|
||
});
|
||
|
||
test('2. R40: 空列表必须 empty=true,禁止注入示例案例数组', () => {
|
||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
|
||
assert.ok(
|
||
content.includes('state.empty = items.length === 0'),
|
||
'loadCases 必须按真实列表长度设置 empty'
|
||
);
|
||
assert.ok(content.includes('state.list = items'), '列表必须使用接口返回数据');
|
||
|
||
// 禁止空列表时回退到本地示例数组
|
||
assert.ok(
|
||
!/state\.list\s*=\s*\[[\s\S]*?title\s*:/.test(content),
|
||
'禁止用内联示例案例数组填充 list'
|
||
);
|
||
assert.ok(!content.includes('DEMO_CASES'), '禁止 DEMO_CASES');
|
||
assert.ok(!content.includes('SAMPLE_CASES'), '禁止 SAMPLE_CASES');
|
||
assert.ok(!content.includes('MOCK_CASES'), '禁止 MOCK_CASES');
|
||
});
|
||
|
||
test('3. R40: 禁止预设假 KPI / 示例客户,metrics 新建为空', () => {
|
||
const uts = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||
|
||
assert.ok(!uts.includes('PRESET_CASE_METRICS'), '禁止 PRESET_CASE_METRICS 假 KPI 常量');
|
||
assert.ok(!page.includes('PRESET_CASE_METRICS'), '页面禁止引用预设 KPI');
|
||
assert.ok(!page.includes('preset-metric-pill'), '禁止预设 KPI 胶囊点击注入');
|
||
assert.ok(!page.includes('handleSelectMetric'), '禁止点击注入预设 KPI');
|
||
|
||
for (const pattern of SAMPLE_BAN_PATTERNS) {
|
||
assert.ok(!pattern.test(uts), `useCardCases 禁止示例数据: ${pattern}`);
|
||
assert.ok(!pattern.test(page), `cases.uvue 禁止示例数据: ${pattern}`);
|
||
}
|
||
|
||
assert.ok(
|
||
/metrics:\s*['"]{2}/.test(page) || page.includes("editForm.metrics = ''"),
|
||
'新建时 metrics 必须置为空字符串'
|
||
);
|
||
});
|
||
|
||
test('4. 案例表单校验逻辑实现规范', () => {
|
||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
assert.ok(content.includes("title === ''"), '必须拦截空标题');
|
||
assert.ok(content.includes('title.length > 150'), '必须拦截超过 150 字符的标题');
|
||
});
|
||
|
||
test('5. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
assert.ok(content.includes('fetchCardCasesPage'), '必须调用 fetchCardCasesPage');
|
||
assert.ok(content.includes('addCardCase'), '必须调用 addCardCase');
|
||
assert.ok(content.includes('updateCardCase'), '必须调用 updateCardCase');
|
||
assert.ok(content.includes('deleteCardCase'), '必须调用 deleteCardCase');
|
||
});
|
||
|
||
test('6. 国际化与文案键完整覆盖,模板禁止硬编码中文', () => {
|
||
const uts = fs.readFileSync(casesUtsPath, 'utf-8');
|
||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||
|
||
assert.ok(uts.includes('resolveCasesText'), '必须实现文案解析');
|
||
assert.ok(uts.includes('Portfolio & Case Studies'), '必须支持英文标题');
|
||
assert.ok(uts.includes('产品与案例') || uts.includes('产品相册与标杆案例'), '必须支持中文标题');
|
||
assert.ok(uts.includes('emptyTitle'), '必须含空态标题词条');
|
||
assert.ok(uts.includes('emptyDesc'), '必须含空态说明词条');
|
||
|
||
// 模板可见文案走 t() / resolveCasesText,禁止 placeholder 硬编码中文示例
|
||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||
assert.ok(templateMatch, '必须有 template 块');
|
||
const template = templateMatch![0];
|
||
assert.ok(
|
||
!/placeholder="[^"]*[\u4e00-\u9fff]/.test(template),
|
||
'模板 placeholder 禁止硬编码中文'
|
||
);
|
||
assert.ok(template.includes("t('emptyTitle')"), '空态标题必须走 i18n');
|
||
assert.ok(template.includes("t('emptyDesc')"), '空态说明必须走 i18n');
|
||
assert.ok(template.includes("t('addBtn')"), '主 CTA 文案必须走 i18n');
|
||
});
|
||
|
||
test('7. R40: 空列表展示空态,一屏仅 1 个主 CTA', () => {
|
||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||
|
||
assert.ok(page.includes('state.empty'), '必须根据 state.empty 分支');
|
||
assert.ok(page.includes('cases-empty-card'), '必须有空态卡片');
|
||
assert.ok(page.includes("t('emptyTitle')"), '空态必须显示 emptyTitle');
|
||
assert.ok(page.includes("t('emptyDesc')"), '空态必须显示 emptyDesc');
|
||
|
||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||
assert.ok(templateMatch, '必须有 template 块');
|
||
const template = templateMatch![0];
|
||
|
||
// 主屏底部仅一处 primary-cta-btn(抽屉内保存可另计,但主列表区底部栏只能 1 个)
|
||
const bottomBarMatch = template.match(
|
||
/cases-bottom-bar[\s\S]*?<\/view>\s*(?:<!--[\s\S]*?-->\s*)*<view v-if="state\.drawerVisible"/
|
||
);
|
||
assert.ok(bottomBarMatch, '必须存在底部主 CTA 栏');
|
||
const bottomBar = bottomBarMatch![0];
|
||
const ctaInBar = (bottomBar.match(/class="primary-cta-btn"/g) || []).length;
|
||
assert.equal(ctaInBar, 1, '底部栏一屏仅允许 1 个 primary-cta-btn');
|
||
assert.ok(bottomBar.includes('handleOpenAdd'), '主 CTA 必须是新增案例');
|
||
});
|