forked from wangziqi/gongxue-base
test: strengthen launch smoke and capacity guardrails
This commit is contained in:
@@ -22,6 +22,19 @@ const ids = {
|
||||
session: '00000000-0000-4000-8000-000000000601',
|
||||
plan: '00000000-0000-4000-8000-000000000701',
|
||||
order: '00000000-0000-4000-8000-000000000801',
|
||||
vocabularyUnit: '00000000-0000-4000-8000-000000000901',
|
||||
vocabularyWord: '00000000-0000-4000-8000-000000000902',
|
||||
handbookSubject: '00000000-0000-4000-8000-000000001001',
|
||||
handbookChapter: '00000000-0000-4000-8000-000000001002',
|
||||
handbookEntry: '00000000-0000-4000-8000-000000001003',
|
||||
asset: '00000000-0000-4000-8000-000000001101',
|
||||
video: '00000000-0000-4000-8000-000000001201',
|
||||
school: '00000000-0000-4000-8000-000000001301',
|
||||
major: '00000000-0000-4000-8000-000000001302',
|
||||
scorelineField: '00000000-0000-4000-8000-000000001303',
|
||||
scorelineRecord: '00000000-0000-4000-8000-000000001304',
|
||||
notification: '00000000-0000-4000-8000-000000001401',
|
||||
aiReport: '00000000-0000-4000-8000-000000001501',
|
||||
};
|
||||
|
||||
const portals = [
|
||||
@@ -210,7 +223,8 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
phone: '13800000000',
|
||||
avatarPreset: 'male',
|
||||
score: 120,
|
||||
target: { regionId: ids.region, regionName: '天津' },
|
||||
target: { regionId: ids.region, regionName: '天津', schoolId: ids.school, schoolName: '天津职业大学', majorId: ids.major, majorName: '软件工程' },
|
||||
membership: { isSvip: true },
|
||||
stats: { answers: { totalAnswered: 12 } },
|
||||
},
|
||||
};
|
||||
@@ -218,7 +232,23 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
if (pathname === '/api/profile/score-events') return { items: [] };
|
||||
if (pathname === '/api/profile/activity-tasks') return { items: [] };
|
||||
if (pathname === '/api/profile/exchange-items') return { items: [] };
|
||||
if (pathname === '/api/profile/notifications') return { items: [], summary: { unread: 0, read: 0, archived: 0 } };
|
||||
if (pathname === '/api/profile/notifications') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.notification,
|
||||
notificationType: 'badge_granted',
|
||||
status: query.get('status') || 'unread',
|
||||
severity: 'success',
|
||||
title: '入门勋章已发放',
|
||||
message: '完成首次练习后,系统已自动发放坚持练习勋章。',
|
||||
actionLabel: '查看个人中心',
|
||||
actionPath: '/pages/student/profile/index',
|
||||
createdAt: new Date().toISOString(),
|
||||
}],
|
||||
summary: { unread: 1, read: 0, archived: 0, dismissed: 0 },
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/profile/notifications/status') return { item: { updatedCount: body.notificationIds?.length || 1, status: body.status || 'read' } };
|
||||
if (pathname === '/api/profile/badges') return { items: [{ id: 'badge-smoke', name: '坚持练习' }] };
|
||||
if (pathname === '/api/profile/exam-countdowns') return { items: [] };
|
||||
if (pathname === '/api/commerce/entitlements') return { items: [{ type: 'svip', status: 'active' }], summary: { svip: true } };
|
||||
@@ -270,8 +300,21 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
}
|
||||
if (pathname === '/api/learning/favorites/questions' && method === 'POST') return { ok: true, favorite: body.favorite !== false };
|
||||
if (pathname === '/api/learning/favorites/questions') return { items: [question] };
|
||||
if (pathname === '/api/learning/wrong-questions/review-plan') return { items: [], nextAction: {} };
|
||||
if (pathname === '/api/learning/wrong-questions') return { items: [] };
|
||||
if (pathname === '/api/learning/wrong-questions/review-plan') {
|
||||
return {
|
||||
items: [{
|
||||
questionId: ids.question,
|
||||
content: question.content,
|
||||
type: question.type,
|
||||
typeLabel: question.typeLabel,
|
||||
wrongCount: 2,
|
||||
lastWrongAt: new Date().toISOString(),
|
||||
}],
|
||||
nextAction: { mode: 'wrong_review', questionLimit: 30 },
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/learning/wrong-questions/resolve') return { ok: true };
|
||||
if (pathname === '/api/learning/wrong-questions') return { items: [question] };
|
||||
if (pathname === '/api/learning/stats') {
|
||||
return {
|
||||
item: {
|
||||
@@ -288,6 +331,196 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
if (pathname === '/api/learning/trend') return { items: [{ date: '2026-07-01', answeredCount: 3, correctCount: 2, wrongCount: 1, accuracy: 0.66, sessionCount: 1, reportCount: 1, score: 66, totalScore: 100 }] };
|
||||
if (pathname === '/api/learning/practice-sessions/history') return { items: [] };
|
||||
|
||||
if (pathname === '/api/catalog/vocabulary-units') {
|
||||
return { items: [{ id: ids.vocabularyUnit, name: '英语核心词汇', description: '专升本高频词', wordCount: 1, regionId: ids.region }] };
|
||||
}
|
||||
if (pathname === '/api/catalog/vocabulary-words') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.vocabularyWord,
|
||||
wordId: ids.vocabularyWord,
|
||||
unitId: ids.vocabularyUnit,
|
||||
word: 'analysis',
|
||||
phonetic: "ə'næləsɪs",
|
||||
meaning: '分析',
|
||||
example: 'Careful analysis helps students improve.',
|
||||
exampleTranslation: '细致分析能帮助学生提升。',
|
||||
status: 'new',
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/learning/vocabulary/review-plan') {
|
||||
return {
|
||||
item: {
|
||||
words: [{
|
||||
id: ids.vocabularyWord,
|
||||
wordId: ids.vocabularyWord,
|
||||
unitId: ids.vocabularyUnit,
|
||||
word: 'analysis',
|
||||
phonetic: "ə'næləsɪs",
|
||||
meaning: '分析',
|
||||
example: 'Careful analysis helps students improve.',
|
||||
exampleTranslation: '细致分析能帮助学生提升。',
|
||||
dueLevel: 'new',
|
||||
}],
|
||||
dueCount: 1,
|
||||
newCount: 1,
|
||||
totalPlanned: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/learning/vocabulary/stats') {
|
||||
return { item: { totalWords: 1, progressedWords: 0, masteredWords: 0, learningWords: 0, todayReviewed: 0, favoriteWords: 0 } };
|
||||
}
|
||||
if (pathname === '/api/learning/vocabulary/favorites' && method === 'POST') return { ok: true, favorite: body.favorite !== false };
|
||||
if (pathname === '/api/learning/vocabulary/favorites') return { items: [] };
|
||||
if (pathname === '/api/learning/vocabulary/review') return { item: { wordId: body.wordId || ids.vocabularyWord, result: body.result || 'known' } };
|
||||
|
||||
if (pathname === '/api/catalog/handbook-subjects') {
|
||||
return { items: [{ id: ids.handbookSubject, name: '高等数学手册', type: 'handbook', description: '章节化知识点' }] };
|
||||
}
|
||||
if (pathname === '/api/catalog/handbook-chapters') {
|
||||
return { items: [{ id: ids.handbookChapter, subjectId: ids.handbookSubject, name: '函数与极限', description: '基础章节' }] };
|
||||
}
|
||||
if (pathname === '/api/catalog/handbook-entries') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.handbookEntry,
|
||||
chapterId: ids.handbookChapter,
|
||||
title: '导数定义',
|
||||
summary: '导数描述函数在某一点附近的变化率。',
|
||||
content: '设函数 y=f(x),当自变量增量趋近于 0 时,差商的极限称为导数。$f(x)=x^2$ 的导数是 $2x$。',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === '/api/catalog/assets') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.asset,
|
||||
title: '天津专升本英语真题 PDF',
|
||||
assetType: 'pdf',
|
||||
categoryLabel: '真题资料',
|
||||
description: '用于验证私有资料短签名、水印和预览入口。',
|
||||
fileName: 'tj-english-paper.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
visibility: 'members',
|
||||
previewStatus: 'ready',
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/catalog/assets/preview') {
|
||||
return {
|
||||
item: { id: ids.asset, title: '天津专升本英语真题 PDF', assetType: 'pdf', fileName: 'tj-english-paper.pdf', mimeType: 'application/pdf', visibility: 'members' },
|
||||
preview: { url: 'https://assets.example.test/tj-english-paper-preview.pdf?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
|
||||
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-preview-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/catalog/assets/download') {
|
||||
return {
|
||||
item: { id: ids.asset, title: '天津专升本英语真题 PDF', assetType: 'pdf', fileName: 'tj-english-paper.pdf', mimeType: 'application/pdf', visibility: 'members' },
|
||||
download: { url: 'https://assets.example.test/tj-english-paper.pdf?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
|
||||
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-download-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === '/api/questions/videos') {
|
||||
return {
|
||||
videos: [{
|
||||
id: ids.video,
|
||||
questionId: query.get('questionId') || ids.question,
|
||||
title: '本题视频解析',
|
||||
description: '后端签名播放烟测视频',
|
||||
duration: 180,
|
||||
accessMode: 'svip',
|
||||
playable: true,
|
||||
}],
|
||||
total: 1,
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/videos/play') {
|
||||
return {
|
||||
item: { id: body.videoId || ids.video, questionId: body.questionId || ids.question, title: '本题视频解析', duration: 180, accessMode: 'svip' },
|
||||
playToken: 'video-play-token-smoke',
|
||||
playback: { url: 'https://assets.example.test/video-smoke.mp4?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
|
||||
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-video-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
|
||||
access: { accessMode: 'svip', consumedQuota: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === '/api/scoreline/fields') {
|
||||
return { items: [{ id: ids.scorelineField, regionId: ids.region, fieldKey: 'min_score', fieldName: '最低分', fieldType: 'number', unit: '分', isFilter: true, isVisible: true, isTrend: true, sortOrder: 1 }] };
|
||||
}
|
||||
if (pathname === '/api/scoreline/schools') {
|
||||
return { items: [{ id: ids.school, regionId: ids.region, name: '天津职业大学', shortName: '天职大', isHot: true, order: 1 }] };
|
||||
}
|
||||
if (pathname === '/api/scoreline/majors') {
|
||||
return { items: [{ id: ids.major, regionId: ids.region, schoolId: ids.school, name: '软件工程', order: 1, hasRestriction: false }] };
|
||||
}
|
||||
if (pathname === '/api/scoreline/years') return { items: [2026, 2025, 2024] };
|
||||
if (pathname === '/api/scoreline/trend' || pathname === '/api/scoreline/records') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.scorelineRecord,
|
||||
regionId: ids.region,
|
||||
schoolId: ids.school,
|
||||
majorId: ids.major,
|
||||
year: 2025,
|
||||
schoolName: '天津职业大学',
|
||||
majorName: '软件工程',
|
||||
fieldValues: { min_score: 218 },
|
||||
}],
|
||||
total: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (pathname === '/api/ai/school-recommendations') {
|
||||
return {
|
||||
items: [{
|
||||
id: ids.aiReport,
|
||||
tenantId: ids.tenant,
|
||||
userId: ids.user,
|
||||
regionId: ids.region,
|
||||
status: 'generated',
|
||||
provider: 'local_rules',
|
||||
promptVersion: 'smoke-v1',
|
||||
inputPayload: { estimatedScore: 210, riskPreference: 'balanced' },
|
||||
contextPayload: {},
|
||||
resultPayload: {
|
||||
schemaVersion: 'school-recommendation-report-v1',
|
||||
summary: '建议优先关注天津职业大学软件工程方向。',
|
||||
riskLevel: 'balanced',
|
||||
recommendedSchools: [{
|
||||
schoolId: ids.school,
|
||||
schoolName: '天津职业大学',
|
||||
majorId: ids.major,
|
||||
majorName: '软件工程',
|
||||
latestYear: 2025,
|
||||
latestScore: 218,
|
||||
averageScore: 214,
|
||||
scoreGap: -8,
|
||||
riskLevel: 'balanced',
|
||||
confidence: 0.76,
|
||||
reason: '与预估分接近,适合作为稳中带冲选择。',
|
||||
tags: ['天津', '工科'],
|
||||
}],
|
||||
actionPlan: ['复盘公共课错题', '补齐英语词汇'],
|
||||
disclaimers: ['烟测报告仅用于页面联调。'],
|
||||
dataCoverage: { scorelines: 1 },
|
||||
},
|
||||
generatedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}],
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/ai/school-recommendations/generate') {
|
||||
return mockApiPayload('/api/ai/school-recommendations', 'GET', query, body);
|
||||
}
|
||||
if (pathname === '/api/ai/school-recommendations/export') {
|
||||
return { item: { reportId: ids.aiReport, format: query.get('format') || 'markdown', fileName: 'school-report-smoke.md', mimeType: 'text/markdown', contentBase64: 'IyBTbW9rZQ==', contentText: '# Smoke', sha256: 'smoke', sizeBytes: 7 } };
|
||||
}
|
||||
|
||||
if (pathname === '/api/tenant-admin/dashboard') return { item: tenantDashboard };
|
||||
if (pathname === '/api/tenant-admin/overview') return { item: { slug: 'master', brandName: '工学题库商户后台' } };
|
||||
if (pathname === '/api/tenant-admin/permissions') {
|
||||
@@ -740,6 +973,12 @@ async function waitForApiRequest(api, path, method) {
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
async function navigateAndExpect(page, baseUrl, path, text, timeoutMs = 10_000) {
|
||||
await page.navigate(`${baseUrl}${path}`);
|
||||
await waitForPath(page, path.split('?')[0], timeoutMs);
|
||||
await waitForText(page, text, timeoutMs);
|
||||
}
|
||||
|
||||
async function runStudentJourney(browser, portal, api) {
|
||||
const checks = [];
|
||||
const page = await newPage(browser, `${portal.baseUrl}${portal.landingPath}`);
|
||||
@@ -775,6 +1014,61 @@ async function runStudentJourney(browser, portal, api) {
|
||||
await waitForPath(page, '/pages/student/checkout/index');
|
||||
await waitForText(page, '会员收银台');
|
||||
checks.push({ id: 'student.profile.to_checkout', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/review/index?type=wrong', '错题本');
|
||||
await waitForText(page, '开始复习');
|
||||
await waitForText(page, '这是 H5 交互烟测题目');
|
||||
await clickText(page, '收藏');
|
||||
await waitForText(page, '收藏夹');
|
||||
await clickText(page, '开始复习');
|
||||
await waitForPath(page, '/pages/student/practice/index');
|
||||
checks.push({ id: 'student.review.wrong_favorite', status: 'pass', detail: '错题本、收藏夹和后端组卷入口可打开' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/vocabulary/index', '背单词');
|
||||
await waitForText(page, '学习概览');
|
||||
await waitForText(page, 'analysis');
|
||||
await clickText(page, '认识');
|
||||
await waitForApiRequest(api, '/api/learning/vocabulary/review', 'POST');
|
||||
checks.push({ id: 'student.vocabulary.review', status: 'pass', detail: '背单词计划和复习提交 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/handbook/index', '知识手册');
|
||||
await waitForText(page, '高等数学手册');
|
||||
await waitForText(page, '导数定义');
|
||||
await waitForText(page, '变化率');
|
||||
checks.push({ id: 'student.handbook.rendered', status: 'pass', detail: '手册科目、章节和知识点渲染正常' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/assets/index', '资料下载');
|
||||
await waitForText(page, '天津专升本英语真题 PDF');
|
||||
await clickText(page, '预览');
|
||||
await waitForText(page, '追踪码');
|
||||
await waitForApiRequest(api, '/api/catalog/assets/preview', 'GET');
|
||||
await clickText(page, '下载');
|
||||
await waitForText(page, '确认下载');
|
||||
await waitForApiRequest(api, '/api/catalog/assets/download', 'GET');
|
||||
checks.push({ id: 'student.assets.signed_watermark', status: 'pass', detail: '资料预览/下载短签名和水印面板可用' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, `/pages/student/video/index?questionId=${ids.question}`, '视频解析');
|
||||
await waitForText(page, '本题视频解析');
|
||||
await clickText(page, '本题视频解析');
|
||||
await waitForApiRequest(api, '/api/videos/play', 'POST');
|
||||
checks.push({ id: 'student.video.play_auth', status: 'pass', detail: '视频列表和播放授权 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/scoreline/index', '历年分数线');
|
||||
await waitForText(page, '天津职业大学');
|
||||
await waitForText(page, '最低分');
|
||||
checks.push({ id: 'student.scoreline.rendered', status: 'pass', detail: '分数线筛选、趋势和结果渲染正常' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/ai-school/index', 'AI择校推荐');
|
||||
await waitForText(page, '推荐结果');
|
||||
await waitForText(page, '天津职业大学');
|
||||
checks.push({ id: 'student.ai_school.rendered', status: 'pass', detail: 'AI 择校报告列表和推荐结果可渲染' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/student/notifications/index', '消息中心');
|
||||
await waitForText(page, '入门勋章已发放');
|
||||
await clickText(page, '全部已读');
|
||||
await waitForApiRequest(api, '/api/profile/notifications/status', 'POST');
|
||||
checks.push({ id: 'student.notifications.status', status: 'pass', detail: '消息筛选和批量已读 API 已触发' });
|
||||
|
||||
return checks;
|
||||
} finally {
|
||||
page.close();
|
||||
@@ -800,6 +1094,21 @@ async function runTenantJourney(browser, portal) {
|
||||
await waitForPath(page, '/pages/tenant-admin/finance/index');
|
||||
await waitForText(page, '财务运营');
|
||||
checks.push({ id: 'tenant.workbench.to_finance', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
const moduleChecks = [
|
||||
{ label: '数据看板', path: '/pages/tenant-admin/dashboard/index' },
|
||||
{ label: '学生运营', path: '/pages/tenant-admin/students/index' },
|
||||
{ label: '营销中心', path: '/pages/tenant-admin/marketing/index' },
|
||||
{ label: '租户设置', path: '/pages/tenant-admin/settings/index' },
|
||||
];
|
||||
for (const item of moduleChecks) {
|
||||
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
|
||||
await waitForText(page, item.label);
|
||||
await clickText(page, item.label);
|
||||
await waitForPath(page, item.path);
|
||||
await waitForText(page, item.label);
|
||||
checks.push({ id: `tenant.workbench.to_${item.path.split('/').at(-2)}`, status: 'pass', detail: await currentPath(page) });
|
||||
}
|
||||
return checks;
|
||||
} finally {
|
||||
page.close();
|
||||
@@ -825,6 +1134,19 @@ async function runPlatformJourney(browser, portal) {
|
||||
await waitForPath(page, '/pages/platform-admin/billing/index');
|
||||
await waitForText(page, '账务中心');
|
||||
checks.push({ id: 'platform.workbench.to_billing', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
const moduleChecks = [
|
||||
{ label: '公共题库', path: '/pages/platform-admin/question-banks/index', expected: '公共题库授权' },
|
||||
{ label: '平台员工', path: '/pages/platform-admin/staff/index', expected: '平台员工' },
|
||||
];
|
||||
for (const item of moduleChecks) {
|
||||
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
|
||||
await waitForText(page, item.label);
|
||||
await clickText(page, item.label);
|
||||
await waitForPath(page, item.path);
|
||||
await waitForText(page, item.expected);
|
||||
checks.push({ id: `platform.workbench.to_${item.path.split('/').at(-2)}`, status: 'pass', detail: await currentPath(page) });
|
||||
}
|
||||
return checks;
|
||||
} finally {
|
||||
page.close();
|
||||
|
||||
Reference in New Issue
Block a user