文件
OmniCard/mpuniapp/tests/card-articles.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

136 行
6.4 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 articlesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardArticles.uts');
const articlesPagePath = path.join(MP_ROOT, 'pages/owner/card/articles.uvue');
/** 禁止预填的示例文章 / 假标题 / 假封面 */
const SAMPLE_BAN_PATTERNS: RegExp[] = [
/2026\s*全球智能硬件合规白皮书/,
/全球智能硬件合规白皮书/,
/DEMO_ARTICLES/,
/SAMPLE_ARTICLES/,
/MOCK_ARTICLES/,
/placeholder\.png/i,
/fake[-_]?cover/i,
/unsplash\.com/i,
/picsum\.photos/i,
];
test('1. useCardArticles.uts 文件必须存在且格式完整', () => {
assert.ok(fs.existsSync(articlesUtsPath), '必须存在 useCardArticles.uts');
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('export function createArticlesState'), '必须导出 createArticlesState');
assert.ok(content.includes('export function extractArticleSummary'), '必须导出 extractArticleSummary');
assert.ok(content.includes('export function validateArticleForm'), '必须导出 validateArticleForm');
assert.ok(content.includes('export async function loadArticles'), '必须导出 loadArticles');
assert.ok(content.includes('export async function saveArticle'), '必须导出 saveArticle');
assert.ok(content.includes('export async function toggleArticlePublish'), '必须导出 toggleArticlePublish');
assert.ok(content.includes('export async function deleteArticle'), '必须导出 deleteArticle');
});
test('2. 摘要提取器必须支持剥离 Markdown 与 HTML 语法', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('.replace(/<[^>]+>/g, \'\')'), '必须剥离 HTML 标签');
assert.ok(content.includes('.replace(/!\\[.*?\\]\\(.*?\\)/g, \'\')'), '必须剥离 Markdown 图片');
});
test('3. R41: 空列表必须 empty=true,禁止注入示例文章数组', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(
content.includes('state.empty = items.length === 0'),
'loadArticles 必须按真实列表长度设置 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_ARTICLES'), '禁止 DEMO_ARTICLES');
assert.ok(!content.includes('SAMPLE_ARTICLES'), '禁止 SAMPLE_ARTICLES');
assert.ok(!content.includes('MOCK_ARTICLES'), '禁止 MOCK_ARTICLES');
});
test('4. R41: 禁止示例标题 / 假封面,新建表单字段为空', () => {
const uts = fs.readFileSync(articlesUtsPath, 'utf-8');
const page = fs.readFileSync(articlesPagePath, 'utf-8');
for (const pattern of SAMPLE_BAN_PATTERNS) {
assert.ok(!pattern.test(uts), `useCardArticles 禁止示例数据: ${pattern}`);
assert.ok(!pattern.test(page), `articles.uvue 禁止示例数据: ${pattern}`);
}
assert.ok(
page.includes("editForm.title = ''") && page.includes("editForm.content = ''"),
'新建时 title/content 必须置为空字符串'
);
assert.ok(
page.includes("editForm.coverImage = ''") || page.includes("uploadedCover.value = []"),
'新建时封面必须为空,禁止假封面'
);
});
test('5. 文章表单校验逻辑实现规范', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes("title === ''"), '必须拦截空标题');
assert.ok(content.includes("content === ''"), '必须拦截空正文');
});
test('6. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('fetchCardArticlesPage'), '必须调用 fetchCardArticlesPage');
assert.ok(content.includes('addCardArticle'), '必须调用 addCardArticle');
assert.ok(content.includes('updateCardArticle'), '必须调用 updateCardArticle');
assert.ok(content.includes('deleteCardArticle'), '必须调用 deleteCardArticle');
});
test('7. 国际化与文案键完整覆盖,模板禁止硬编码中文', () => {
const uts = fs.readFileSync(articlesUtsPath, 'utf-8');
const page = fs.readFileSync(articlesPagePath, 'utf-8');
assert.ok(uts.includes('resolveArticlesText'), '必须实现文案解析');
assert.ok(uts.includes('Insights & Articles'), '必须支持英文标题');
assert.ok(uts.includes('文章与商务动态'), '必须支持中文标题');
assert.ok(uts.includes('emptyTitle'), '必须含空态标题词条');
assert.ok(uts.includes('emptyDesc'), '必须含空态说明词条');
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('publishBtn')"), '主 CTA 文案必须走 i18n');
});
test('8. R41: 空列表展示空态,一屏仅 1 个主 CTA', () => {
const page = fs.readFileSync(articlesPagePath, 'utf-8');
assert.ok(page.includes('state.empty'), '必须根据 state.empty 分支');
assert.ok(page.includes('articles-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];
const bottomBarMatch = template.match(
/articles-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 必须是发布动态');
assert.ok(bottomBar.includes("t('publishBtn')"), '主 CTA 文案必须是 publishBtn');
});