forked from wangziqi/gongxue-base
test: deepen taro h5 admin smoke and record capacity
This commit is contained in:
@@ -35,6 +35,22 @@ const ids = {
|
||||
scorelineRecord: '00000000-0000-4000-8000-000000001304',
|
||||
notification: '00000000-0000-4000-8000-000000001401',
|
||||
aiReport: '00000000-0000-4000-8000-000000001501',
|
||||
class: '00000000-0000-4000-8000-000000001601',
|
||||
teacher: '00000000-0000-4000-8000-000000001602',
|
||||
tenantMember: '00000000-0000-4000-8000-000000001603',
|
||||
roleTemplate: '00000000-0000-4000-8000-000000001604',
|
||||
importJob: '00000000-0000-4000-8000-000000001701',
|
||||
publicBank: '00000000-0000-4000-8000-000000001801',
|
||||
publicBankGrant: '00000000-0000-4000-8000-000000001802',
|
||||
publicBankAdoption: '00000000-0000-4000-8000-000000001803',
|
||||
crmQueue: '00000000-0000-4000-8000-000000001901',
|
||||
coupon: '00000000-0000-4000-8000-000000002001',
|
||||
pointTask: '00000000-0000-4000-8000-000000002101',
|
||||
pointExchange: '00000000-0000-4000-8000-000000002102',
|
||||
settlement: '00000000-0000-4000-8000-000000002201',
|
||||
proof: '00000000-0000-4000-8000-000000002202',
|
||||
platformStaff: '00000000-0000-4000-8000-000000002301',
|
||||
invoice: '00000000-0000-4000-8000-000000002401',
|
||||
};
|
||||
|
||||
const portals = [
|
||||
@@ -182,6 +198,170 @@ const tenantDashboard = {
|
||||
recentActivities: [{ activityType: 'student_registered', title: '新增学生注册', occurredAt: new Date().toISOString() }],
|
||||
};
|
||||
|
||||
const tenantStudent = {
|
||||
membershipId: 'member-student-smoke',
|
||||
userId: ids.user,
|
||||
username: 'student_smoke',
|
||||
name: '测试学生',
|
||||
phone: '13800000000',
|
||||
email: 'student@example.test',
|
||||
status: 'active',
|
||||
regionId: ids.region,
|
||||
regionName: '天津',
|
||||
selectedSchoolId: ids.school,
|
||||
selectedSchoolName: '天津职业大学',
|
||||
selectedMajorId: ids.major,
|
||||
selectedMajorName: '软件工程',
|
||||
questionsAnsweredToday: 6,
|
||||
masteredWordsCount: 12,
|
||||
classes: [{ classId: ids.class, className: '天津冲刺班' }],
|
||||
};
|
||||
|
||||
const tenantMembers = [
|
||||
{
|
||||
id: ids.tenantMember,
|
||||
membershipId: ids.tenantMember,
|
||||
userId: ids.teacher,
|
||||
username: 'teacher_smoke',
|
||||
name: '测试教师',
|
||||
phone: '13900000000',
|
||||
email: 'teacher@example.test',
|
||||
role: 'teacher',
|
||||
primaryRole: 'teacher',
|
||||
status: 'active',
|
||||
roleTemplateId: ids.roleTemplate,
|
||||
roleTemplateName: '教师运营模板',
|
||||
permissions: { 'students:read': true, 'students:write': true },
|
||||
},
|
||||
{
|
||||
id: 'member-sales-smoke',
|
||||
membershipId: 'member-sales-smoke',
|
||||
userId: 'sales-user-smoke',
|
||||
username: 'sales_smoke',
|
||||
name: '测试销售',
|
||||
phone: '13700000000',
|
||||
role: 'sales',
|
||||
primaryRole: 'sales',
|
||||
status: 'active',
|
||||
commissionRate: 0.2,
|
||||
},
|
||||
];
|
||||
|
||||
const tenantThemeTemplates = [
|
||||
{
|
||||
code: 'classic',
|
||||
name: '经典主题',
|
||||
description: '适合默认题库品牌',
|
||||
theme: { primaryColor: '#2563eb', accentColor: '#0f766e' },
|
||||
},
|
||||
{
|
||||
code: 'focus',
|
||||
name: '专注主题',
|
||||
description: '适合学习型机构',
|
||||
theme: { primaryColor: '#0f766e', accentColor: '#f59e0b' },
|
||||
},
|
||||
{
|
||||
code: 'contrast',
|
||||
name: '高对比主题',
|
||||
description: '适合活动页面',
|
||||
theme: { primaryColor: '#111827', accentColor: '#dc2626' },
|
||||
},
|
||||
];
|
||||
|
||||
const tenantTheme = {
|
||||
activeTemplateCode: 'classic',
|
||||
activeTemplateName: '经典主题',
|
||||
draftTemplateCode: 'focus',
|
||||
draftTemplateName: '专注主题',
|
||||
status: 'draft',
|
||||
theme: { primaryColor: '#2563eb', accentColor: '#0f766e' },
|
||||
publicAssets: { logoUrl: '/assets/logo.svg', shareImageUrl: '/assets/share.png', iconSet: 'classic', shareCardStyle: 'clean' },
|
||||
};
|
||||
|
||||
const tenantPermissionCatalog = {
|
||||
permissions: [
|
||||
{ key: 'students:read', group: 'students', label: '学生查看' },
|
||||
{ key: 'students:write', group: 'students', label: '学生编辑' },
|
||||
{ key: 'content:write', group: 'content', label: '内容编辑' },
|
||||
{ key: 'marketing:write', group: 'marketing', label: '营销编辑' },
|
||||
],
|
||||
menuGroups: [
|
||||
{ key: 'dashboard', group: 'menu', label: '数据看板' },
|
||||
{ key: 'students', group: 'menu', label: '学生运营' },
|
||||
{ key: 'content', group: 'menu', label: '题库内容' },
|
||||
{ key: 'marketing', group: 'menu', label: '营销中心' },
|
||||
{ key: 'settings', group: 'menu', label: '租户设置' },
|
||||
],
|
||||
fieldGroups: [
|
||||
{ key: 'student.phone', group: 'student', label: '学生手机号' },
|
||||
{ key: 'student.email', group: 'student', label: '学生邮箱' },
|
||||
],
|
||||
};
|
||||
|
||||
const importJob = {
|
||||
id: ids.importJob,
|
||||
importType: 'questions',
|
||||
sourceFormat: 'json',
|
||||
status: 'previewed',
|
||||
sourceName: 'smoke-questions.json',
|
||||
totalCount: 1,
|
||||
validCount: 1,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
insertedCount: 0,
|
||||
updatedCount: 0,
|
||||
skippedCount: 0,
|
||||
executionMode: 'sync',
|
||||
attemptCount: 0,
|
||||
maxAttempts: 3,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
summary: { source: 'h5-smoke' },
|
||||
};
|
||||
|
||||
const publicQuestionBank = {
|
||||
grantId: ids.publicBankGrant,
|
||||
sourceQuestionBankId: ids.publicBank,
|
||||
sourceQuestionBankName: '平台公共题库',
|
||||
sourceRegionName: '天津',
|
||||
name: '平台公共题库',
|
||||
regionName: '天津',
|
||||
grantScope: 'plan_code',
|
||||
questionCount: 128,
|
||||
adoptedId: ids.publicBankAdoption,
|
||||
adoptionStatus: 'active',
|
||||
syncStatus: 'conflict',
|
||||
copiedQuestionCount: 120,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const publicBankConflicts = {
|
||||
adoptionId: ids.publicBankAdoption,
|
||||
syncStatus: 'conflict',
|
||||
status: 'conflict',
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
conflictCount: 1,
|
||||
counts: { conflict: 1 },
|
||||
conflicts: [{
|
||||
sourceQuestionId: ids.question,
|
||||
targetQuestionId: 'target-question-smoke',
|
||||
action: 'update_conflict',
|
||||
sourceHash: 'platformhash',
|
||||
previousSourceHash: 'oldhash',
|
||||
targetHash: 'tenanthash',
|
||||
}],
|
||||
};
|
||||
|
||||
const platformPermissionCatalog = [
|
||||
{ key: '*', group: 'staff', label: '超级权限' },
|
||||
{ key: 'platform:staff:write', group: 'staff', label: '员工编辑' },
|
||||
{ key: 'platform:staff:status', group: 'staff', label: '员工状态' },
|
||||
{ key: 'platform:tenant:write', group: 'tenant', label: '租户编辑' },
|
||||
{ key: 'platform:billing:write', group: 'billing', label: '账务编辑' },
|
||||
{ key: 'platform:question_bank:write', group: 'question_bank', label: '题库授权' },
|
||||
];
|
||||
|
||||
function mockApiPayload(pathname, method, query, body) {
|
||||
if (pathname === '/health') return { ok: true };
|
||||
if (pathname === '/api/tenant/resolve') return baseTenantPayload(query);
|
||||
@@ -522,9 +702,16 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
}
|
||||
|
||||
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/overview') {
|
||||
return { item: { id: ids.tenant, slug: 'master', status: 'active', brandName: '工学题库商户后台', shortName: '工学', billingStatus: 'normal' } };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/theme-templates') return { items: tenantThemeTemplates };
|
||||
if (pathname === '/api/tenant-admin/theme') return { item: tenantTheme };
|
||||
if (pathname === '/api/tenant-admin/theme/preview') return { item: { ...tenantTheme, draftTemplateCode: body.templateCode || 'focus', templateName: '专注主题', status: 'draft' } };
|
||||
if (pathname === '/api/tenant-admin/theme/publish') return { item: { ...tenantTheme, activeTemplateCode: body.templateCode || tenantTheme.draftTemplateCode, templateName: '专注主题', status: 'published' } };
|
||||
if (pathname === '/api/tenant-admin/permissions') {
|
||||
return {
|
||||
...tenantPermissionCatalog,
|
||||
current: {
|
||||
role: 'owner',
|
||||
menuPermissions: { dashboard: true, students: true, content: true, marketing: true, commerce: true, settings: true },
|
||||
@@ -533,24 +720,132 @@ function mockApiPayload(pathname, method, query, body) {
|
||||
roleDefaults: { owner: ['*'] },
|
||||
};
|
||||
}
|
||||
if (pathname === '/api/tenant-content/content-entries') return { items: [{ id: ids.entry, name: '天津专升本题库', entryType: 'question_bank', status: 'active' }] };
|
||||
if (pathname === '/api/tenant-content/public-question-banks') return { items: [{ grantId: 'grant-smoke', questionBankId: 'bank-smoke', name: '平台公共题库', regionName: '天津', grantScope: '套餐授权', questionCount: 128 }] };
|
||||
if (pathname === '/api/tenant-content/imports/templates') return { item: { importType: query.get('importType') || 'questions', format: query.get('format') || 'json', content: '{}' } };
|
||||
if (pathname === '/api/tenant-content/imports/field-mapping') return { item: { importType: query.get('importType') || 'questions', aliases: {}, allowedTargets: [] } };
|
||||
if (pathname === '/api/tenant-admin/classes') {
|
||||
return { scoped: false, items: [{ id: ids.class, name: '天津冲刺班', code: 'TJ-01', status: 'active', regionName: '天津', studentCount: 32, teacherCount: 2 }] };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/students' && method === 'GET') return { scoped: false, items: [tenantStudent] };
|
||||
if (pathname === '/api/tenant-admin/students' && method === 'PUT') return { item: { ...tenantStudent, ...body, userId: body.userId || ids.user } };
|
||||
if (pathname === '/api/tenant-admin/students/bulk-upsert') return { total: body.students?.length || 1, successCount: body.students?.length || 1, errorCount: 0, items: body.students || [] };
|
||||
if (pathname === '/api/tenant-admin/students/status') return { item: { userId: body.userId || ids.user, status: body.status || 'active' } };
|
||||
if (pathname === '/api/tenant-admin/students/crm-push') return { requestId: 'crm-push-smoke', total: body.studentUserIds?.length || 1, successCount: body.studentUserIds?.length || 1, errorCount: 0 };
|
||||
if (pathname === '/api/tenant-admin/classes/members/bulk-assign') return { total: body.assignments?.length || 1, successCount: body.assignments?.length || 1, errorCount: 0 };
|
||||
if (pathname === '/api/tenant-admin/teachers') return { items: [{ userId: ids.teacher, username: 'teacher_smoke', name: '测试教师', phone: '13900000000', status: 'active', classCount: 1 }] };
|
||||
if (pathname === '/api/tenant-admin/students/notes' && method === 'GET') return { items: [{ id: 'note-smoke', studentUserId: ids.user, noteType: 'learning', content: '烟测备注', visibility: 'tenant_staff', isPinned: false, createdByName: '测试教师', createdAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/tenant-admin/students/notes' && method === 'PUT') return { item: { id: 'note-smoke-created', ...body, createdAt: new Date().toISOString() } };
|
||||
if (pathname === '/api/tenant-admin/students/followups/report') {
|
||||
return { item: { summary: { total: 6, openBacklog: 2, overdueBacklog: 1, done: 3, crmPushTasks: 2, completionRate: 0.5, avgCompleteHours: 8 }, byAssignee: [{ assignedToUserId: ids.teacher, assignedToName: '测试教师', total: 4, open: 1, done: 3, overdue: 1 }], overdueItems: [] } };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/students/followups' && method === 'GET') {
|
||||
return { items: [{ id: 'followup-smoke', studentUserId: ids.user, studentName: '测试学生', title: '学习跟进', followupType: 'learning', priority: 'normal', status: 'open', assignedToUserId: ids.teacher, assignedToName: '测试教师' }] };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/students/followups' && method === 'PUT') return { item: { id: body.id || 'followup-created-smoke', ...body } };
|
||||
if (pathname === '/api/tenant-admin/students/supervision/preview') {
|
||||
return { item: { rules: {}, filters: {}, totalCandidates: 1, candidates: [{ studentUserId: ids.user, studentName: '测试学生', title: '学习风险提醒', description: '近期答题偏少', followupType: 'learning', priority: 'high', riskScore: 86, reasons: [{ code: 'low_activity' }] }] } };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/students/supervision/rules' && method === 'GET') return { scoped: false, items: [{ id: 'supervision-rule-smoke', name: '全租户 学习督导', status: 'active', className: null, lastResult: { status: 'ok' } }] };
|
||||
if (pathname === '/api/tenant-admin/students/supervision/rules' && method === 'PUT') return { item: { id: 'supervision-rule-created-smoke', ...body, status: body.status || 'active' } };
|
||||
if (pathname === '/api/tenant-admin/students/supervision/generate') return { total: body.studentUserIds?.length || 1, successCount: body.studentUserIds?.length || 1, errorCount: 0 };
|
||||
if (pathname === '/api/tenant-admin/role-templates' && method === 'GET') {
|
||||
return { items: [{ id: ids.roleTemplate, code: 'teacher-ops', name: '教师运营模板', baseRole: 'teacher', status: 'active', sortOrder: 10, permissions: { 'students:read': true }, menuPermissions: { students: true }, modulePermissions: {}, fieldPermissions: {}, dataScope: { mode: 'class' }, isSystem: false }] };
|
||||
}
|
||||
if (pathname === '/api/tenant-admin/role-templates' && method === 'PUT') return { item: { id: body.id || 'role-template-created-smoke', ...body } };
|
||||
if (pathname === '/api/tenant-admin/role-templates/disable') return { item: { id: body.roleTemplateId, status: 'disabled' } };
|
||||
if (pathname === '/api/tenant-admin/members' && method === 'GET') return { items: tenantMembers };
|
||||
if (pathname === '/api/tenant-admin/members' && method === 'PUT') return { item: { id: body.membershipId || 'member-created-smoke', membershipId: body.membershipId || 'member-created-smoke', userId: body.userId || ids.teacher, ...body } };
|
||||
if (pathname === '/api/tenant-admin/members/disable') return { item: { id: body.membershipId, status: 'disabled' } };
|
||||
if (pathname === '/api/tenant-admin/domains') return { items: [{ id: 'domain-smoke', host: 'tiku.example.test', domainType: 'custom', status: 'verified', isPrimary: true }] };
|
||||
if (pathname === '/api/tenant-admin/payment-accounts') return { items: [{ id: 'payment-smoke', provider: 'wechat_pay', status: 'active', mode: 'tenant_collect' }] };
|
||||
if (pathname === '/api/tenant-admin/auth-providers') return { items: [{ id: 'auth-wechat-smoke', provider: 'wechat_miniapp', status: 'active' }] };
|
||||
|
||||
if (pathname === '/api/tenant-content/content-entries') return { items: [{ id: ids.entry, name: '天津专升本题库', entryType: 'question_bank', visibility: 'tenant', isActive: true, regionName: '天津', status: 'active' }] };
|
||||
if (pathname === '/api/tenant-content/imports') return { items: [importJob] };
|
||||
if (pathname === '/api/tenant-content/imports/detail') return { item: importJob, issueSummary: {}, itemStatusSummary: { valid: 1 }, recentIssues: [], importPostCheck: { status: 'passed', issues: [] }, worker: { executionMode: 'sync', attemptCount: 0, maxAttempts: 3 } };
|
||||
if (pathname === '/api/tenant-content/imports/issues') return { items: [] };
|
||||
if (pathname === '/api/tenant-content/imports/templates') {
|
||||
const importType = query.get('importType') || 'questions';
|
||||
const format = query.get('format') || 'json';
|
||||
return { item: { importType, format, fileName: `${importType}-template.${format}`, mimeType: 'application/json', contentBase64: 'W3sidGl0bGUiOiJzbW9rZSJ9XQ==', contentPreview: '[{"title":"smoke","type":"single_choice"}]', fields: [{ field: 'title', label: '题干', required: true, aliases: ['题干', 'title'] }] } };
|
||||
}
|
||||
if (pathname === '/api/tenant-content/imports/field-mapping') return { item: { importType: query.get('importType') || 'questions', title: '导入字段', fields: [{ field: 'title', label: '题干', required: true, aliases: ['题干', 'title'] }], requiredFields: ['title'] } };
|
||||
if (pathname.startsWith('/api/tenant-content/imports/preview/')) return { job: { ...importJob, id: ids.importJob }, items: [{ rowNo: 1, status: 'valid', externalId: 'smoke-question', normalized: { title: 'smoke' }, issues: [] }], issues: [] };
|
||||
if (/^\/api\/tenant-content\/imports\/(questions|vocabulary|handbook|scoreline|videos)$/.test(pathname)) return { item: { ...importJob, id: ids.importJob, jobId: ids.importJob, status: 'completed', successCount: 1 }, preview: { job: importJob } };
|
||||
if (pathname === '/api/tenant-content/imports/post-check') return { item: { status: 'passed', issues: [], checkedAt: new Date().toISOString() } };
|
||||
if (pathname === '/api/tenant-content/public-question-banks') return { items: [publicQuestionBank] };
|
||||
if (pathname === '/api/tenant-content/public-question-banks/adopt') return { item: { id: ids.publicBankAdoption, grantId: body.grantId || ids.publicBankGrant, status: 'active' } };
|
||||
if (pathname === '/api/tenant-content/public-question-banks/sync') return { item: { id: body.adoptionId || ids.publicBankAdoption, syncStatus: 'conflict' }, sync: publicBankConflicts };
|
||||
if (pathname === '/api/tenant-content/public-question-banks/conflicts') return { item: publicBankConflicts };
|
||||
if (pathname === '/api/tenant-content/public-question-banks/conflicts/resolve') return { item: { adoptionId: body.adoptionId, sourceQuestionId: body.sourceQuestionId, resolution: body.resolution, status: 'resolved' } };
|
||||
if (pathname === '/api/tenant-content/public-question-banks/conflicts/resolve-batch') return { item: { adoptionId: body.adoptionId, resolvedCount: body.sourceQuestionIds?.length || 1, resolution: body.resolution } };
|
||||
if (pathname === '/api/tenant-content/notifications') return { items: [{ id: 'content-notification-smoke', notificationType: 'public_bank_conflict', status: 'unread', severity: 'warning', adoptionId: ids.publicBankAdoption, title: '公共题库存在同步冲突', message: '平台公共题库有 1 道题需要处理。', createdAt: new Date().toISOString() }], summary: { unread: 1 } };
|
||||
if (pathname === '/api/tenant-content/notifications/status') return { item: { updatedCount: body.notificationIds?.length || 1, status: body.status } };
|
||||
|
||||
if (pathname === '/api/tenant-admin/code-batches') return { items: [{ id: 'batch-smoke', name: 'SVIP 激活码批次', status: 'active', totalCount: 100, usedCount: 18 }] };
|
||||
if (pathname === '/api/tenant-admin/activation-codes') return { items: [{ id: 'code-smoke', code: 'SMOKE2026', status: 'unused' }] };
|
||||
if (pathname === '/api/tenant-admin/coupons' && method === 'GET') return { items: [{ id: ids.coupon, code: 'SMOKE80', status: 'active', campaignName: '暑期活动', discountType: 'fixed', discountValue: 8000, usedCount: 3, maxUses: 100, minOrderAmountCents: 10000, perUserLimit: 1 }] };
|
||||
if (pathname === '/api/tenant-admin/coupons' && method === 'PUT') return { item: { id: body.id || ids.coupon, ...body, usedCount: 0 } };
|
||||
if (pathname === '/api/tenant-admin/coupons/redemptions') return { items: [{ id: 'redemption-smoke', couponId: ids.coupon, couponCode: 'SMOKE80', userId: ids.user, userName: '测试学生', status: 'used', discountAppliedCents: 8000, planName: 'SVIP 年卡', orderNo: 'SMOKE202607010001' }] };
|
||||
if (pathname === '/api/tenant-admin/coupons/report') return { item: { claimCount: 12, usedCount: 3, discountCents: 24000, paidAmountCents: 59700, conversionRate: 0.25, byCampaign: [{ campaignName: '暑期活动', claimCount: 12, usedCount: 3, discountCents: 24000 }] } };
|
||||
if (pathname === '/api/tenant-admin/point-activity-tasks' && method === 'GET') return { items: [{ id: ids.pointTask, code: 'daily-smoke', title: '每日练习', taskType: 'practice_complete', rewardPoints: 10, claimLimitPerUser: 1, periodType: 'daily', status: 'active', claimCount: 4, claimUserCount: 3 }] };
|
||||
if (pathname === '/api/tenant-admin/point-activity-tasks' && method === 'PUT') return { item: { id: body.id || ids.pointTask, ...body, claimCount: 0, claimUserCount: 0 } };
|
||||
if (pathname === '/api/tenant-admin/point-activity-claims') return { items: [{ id: 'claim-smoke', taskTitle: '每日练习', rewardPoints: 10, userName: '测试学生', periodKey: '2026-07-01' }] };
|
||||
if (pathname === '/api/tenant-admin/point-exchange-items' && method === 'GET') return { items: [{ id: ids.pointExchange, code: 'manual-gift', title: '人工兑换礼品', costPoints: 100, itemType: 'manual', stockRemaining: 20, status: 'active', orderCount: 1 }] };
|
||||
if (pathname === '/api/tenant-admin/point-exchange-items' && method === 'PUT') return { item: { id: body.id || ids.pointExchange, ...body, stockRemaining: body.stockTotal || null, orderCount: 0 } };
|
||||
if (pathname === '/api/tenant-admin/point-exchange-orders') return { items: [{ id: 'exchange-order-smoke', itemTitle: '人工兑换礼品', status: 'pending_fulfillment', userName: '测试学生', costPoints: 100, createdAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/tenant-admin/points-risk-report') return { item: { summary: { riskScore: 8, suspiciousUserCount: 0, suspiciousEventCount: 0, netPoints: 120 }, suspiciousUsers: [], suspiciousEvents: [], suspiciousTasks: [] } };
|
||||
if (pathname === '/api/tenant-admin/feedbacks/report') return { item: { summary: { total: 5, pendingBacklog: 1, handledRate: 0.8, rewardPoints: 20, highPriority: 1, resolved: 2, closed: 1, avgHandleHours: 4, resolutionRate: 0.6 }, recentUnhandled: [], topQuestions: [] } };
|
||||
if (pathname === '/api/tenant-admin/user-notifications') return { items: [{ id: 'user-notification-smoke', userId: ids.user, userName: '测试学生', notificationType: 'badge_granted', status: 'unread', title: '勋章发放', message: '坚持练习勋章已发放', createdAt: new Date().toISOString() }], summary: { unread: 1, archived: 0 } };
|
||||
if (pathname === '/api/crm/config' && method === 'GET') return { item: { enabled: true, url: 'https://crm.example.test/webhook', secretRef: 'app_private.tenant_secrets:crm:webhook', formName: '刷题题库', examType: '专升本', timeoutSec: 10, delaySec: 60, assignmentMode: 'referrer', assignmentPool: ['sales-user-smoke'], assignmentCursor: 0 } };
|
||||
if (pathname === '/api/crm/config' && method === 'PUT') return { item: { id: 'crm-config-smoke', ...body } };
|
||||
if (pathname === '/api/crm/queue') return { items: [{ id: ids.crmQueue, status: 'failed', source: 'student.crm_push', attempts: 3, lastHttpCode: 500, lastError: 'smoke failed webhook', targetUrl: 'https://crm.example.test/webhook', provider: 'webhook', createdAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/crm/dead-letters') return { summary: { total: 1, failed: 1, ignored: 0 }, items: [{ id: ids.crmQueue, status: 'failed', source: 'student.crm_push', attempts: 3, lastError: 'smoke failed webhook', deadLetteredAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/crm/queue/logs') return { item: { id: query.get('queueId') || ids.crmQueue }, items: [{ id: 'crm-log-smoke', outcome: 'failed', attempt: 3, httpCode: 500, errorMessage: 'smoke failed webhook', createdAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/crm/queue/action') return { item: { id: body.queueId || ids.crmQueue, status: body.action === 'ignore' ? 'discarded' : 'pending', lastOperatorAction: body.action } };
|
||||
if (pathname === '/api/commission/settings' && method === 'GET') return { item: { defaultRate: 0.2, minSettlementCents: 0, settlementCycle: 'monthly' } };
|
||||
if (pathname === '/api/commission/settings' && method === 'PUT') return { item: { ...body } };
|
||||
if (pathname === '/api/commission/member-rate') return { item: { userId: body.userId, commissionRate: body.commissionRate } };
|
||||
if (pathname === '/api/commission/summary') return { item: { sourceCount: 3, grossAmountCents: 59700, commissionAmountCents: 11940, paidUserCount: 3 } };
|
||||
if (pathname === '/api/referral/conversion-report') return { item: { summary: { leadCount: 10, convertedLeadCount: 3, conversionRate: 0.3, grossAmountCents: 59700, commissionAmountCents: 11940, crmFailedCount: 1, openFollowupCount: 2, avgFirstPayHours: 24 }, funnel: [{ key: 'lead', label: '客资', count: 10 }], byReferrer: [{ referrerUserId: 'sales-user-smoke', name: '测试销售', role: 'sales', leadCount: 10, convertedLeadCount: 3, conversionRate: 0.3, grossAmountCents: 59700, commissionAmountCents: 11940, crmFailedCount: 1, openFollowupCount: 2, overdueFollowupCount: 1, avgFirstPayHours: 24 }], dailyTrend: [] } };
|
||||
if (pathname === '/api/commission/orders') return { items: [{ referrerUserId: 'sales-user-smoke', referrerName: '测试销售', grossAmountCents: 19900, commissionRate: 0.2, commissionAmountCents: 3980, sourceType: 'referral', sourceId: 'source-smoke' }] };
|
||||
if (pathname === '/api/commission/settlements' && method === 'GET') return { items: [{ id: ids.settlement, settlementNo: 'SETTLE-SMOKE', referrerUserId: 'sales-user-smoke', referrerName: '测试销售', status: 'pending_review', commissionAmountCents: 3980, periodStart: '2026-07-01', periodEnd: '2026-07-31', sourceCount: 1 }] };
|
||||
if (pathname === '/api/commission/settlements/generate') return { item: { id: ids.settlement, ...body, settlementNo: 'SETTLE-SMOKE', commissionAmountCents: 3980 } };
|
||||
if (pathname === '/api/commission/settlements/status') return { item: { id: body.settlementId || ids.settlement, status: body.status } };
|
||||
if (pathname === '/api/commission/settlements/export') return { item: { filename: 'settlement-smoke.csv', format: 'csv', mimeType: 'text/csv', contentBase64: 'aWQsc3RhdHVzCnNtb2tlLHBhaWQK', rowCount: 1 } };
|
||||
if (pathname === '/api/commission/settlements/proofs' && method === 'GET') return { items: [{ id: ids.proof, settlementId: query.get('settlementId') || ids.settlement, title: '烟测凭证', status: 'submitted', externalUrl: 'https://assets.example.test/proof.pdf', amountCents: 3980 }] };
|
||||
if (pathname === '/api/commission/settlements/proofs' && method === 'POST') return { item: { id: ids.proof, status: 'submitted', ...body } };
|
||||
if (pathname === '/api/commission/settlements/proofs/status') return { item: { id: body.proofId || ids.proof, status: body.status } };
|
||||
if (pathname.startsWith('/api/tenant-content/') || pathname.startsWith('/api/tenant-admin/') || pathname.startsWith('/api/crm/') || pathname.startsWith('/api/commission/') || pathname.startsWith('/api/referral/')) {
|
||||
return method === 'GET' ? { items: [], item: null, summary: {} } : { ok: true, item: { id: 'tenant-smoke' } };
|
||||
}
|
||||
|
||||
if (pathname === '/api/platform-admin/overview') {
|
||||
return { item: { tenants: { total: 6, active: 5 }, billing: { unpaidAmountCents: 360000 }, subscriptions: { expiringSoon: 1 }, usage: { questions: 74117 } } };
|
||||
return { item: { tenants: { total: 6, active: 5 }, billing: { unpaidAmountCents: 360000, paidAmountCents: 1200000 }, subscriptions: { expiringSoon: 1 }, usage: { questions: 74117, students: 3722 } } };
|
||||
}
|
||||
if (pathname === '/api/platform-admin/permissions') return { item: { primaryRole: 'platform_admin', effective: { '*': true }, catalog: [] } };
|
||||
if (pathname === '/api/platform-admin/plans') return { items: [{ id: ids.plan, code: 'starter_yearly', name: '基础年费', baseAmountCents: 199900, status: 'active' }] };
|
||||
if (pathname === '/api/platform-admin/tenants') return { items: [{ id: ids.tenant, slug: 'master', name: '工学题库', brandName: '工学题库', status: 'active', billingStatus: 'normal', planCode: 'starter_yearly', openBalanceCents: 0 }] };
|
||||
if (pathname === '/api/platform-admin/invoices') return { items: [{ id: 'invoice-smoke', tenantId: ids.tenant, invoiceNo: 'INV-SMOKE', status: 'unpaid', totalCents: 199900, balanceCents: 199900 }] };
|
||||
if (pathname === '/api/platform-admin/question-banks') return { items: [{ id: 'bank-smoke', name: '平台公共题库', status: 'active', questionCount: 128 }] };
|
||||
if (pathname === '/api/platform-admin/question-bank-grants') return { items: [{ id: 'grant-smoke', questionBankId: 'bank-smoke', grantScope: 'all_active_tenants', status: 'active' }] };
|
||||
if (pathname === '/api/platform-admin/staff') return { items: [{ id: ids.user, name: '平台管理员', primaryRole: 'platform_admin', status: 'active', authUserId: 'auth-smoke' }] };
|
||||
if (pathname === '/api/platform-admin/permissions') return { item: { primaryRole: 'platform_admin', effective: { '*': true, 'platform:staff:write': true, 'platform:staff:status': true }, catalog: platformPermissionCatalog } };
|
||||
if (pathname === '/api/platform-admin/plans') return { items: [{ id: ids.plan, code: 'starter_yearly', name: '基础年费', billingCycle: 'yearly', baseAmountCents: 199900, currency: 'CNY', status: 'active' }] };
|
||||
if (pathname === '/api/platform-admin/tenants' && method === 'GET') return { items: [{ id: ids.tenant, slug: 'master', name: '工学题库', legalName: '工学教育科技有限公司', brandName: '工学题库', status: 'active', billingStatus: 'normal', planCode: 'starter_yearly', subscriptionStatus: 'active', subscriptionExpiresAt: '2027-07-01T00:00:00.000Z', openBalanceCents: 199900 }] };
|
||||
if (pathname === '/api/platform-admin/tenants' && method === 'POST') return { item: { id: ids.tenant, slug: body.slug || 'smoke-tenant', name: body.name || '烟测租户', brandName: body.brandName || body.name || '烟测租户', status: body.status || 'active', billingStatus: body.billingStatus || 'trial', planCode: body.planCode || 'starter_yearly' } };
|
||||
if (pathname === '/api/platform-admin/tenants/detail') return { item: { tenant: { id: ids.tenant, slug: 'master', name: '工学题库', legalName: '工学教育科技有限公司', brandName: '工学题库', status: 'active', billingStatus: 'normal', billingName: '工学教育科技有限公司', contactName: '陈老师', contactPhone: '13800000000', invoiceType: 'normal_vat' }, domains: [{ id: 'domain-smoke', host: 'tiku.example.test', status: 'verified', isPrimary: true }], subscriptions: [{ id: 'subscription-smoke', planCode: 'starter_yearly', status: 'active', amountCents: 199900, startsAt: '2026-07-01', expiresAt: '2027-07-01' }], invoices: [{ id: ids.invoice, tenantId: ids.tenant, invoiceNo: 'INV-SMOKE', status: 'unpaid', totalCents: 199900, paidCents: 0, balanceCents: 199900, dueDate: '2026-07-10' }], usage: [{ id: 'usage-smoke', tenantId: ids.tenant, metricKey: 'students', metricValue: 3722, periodStart: '2026-07-01', periodEnd: '2026-07-31' }] } };
|
||||
if (pathname === '/api/platform-admin/tenants/status') return { item: { id: body.tenantId || ids.tenant, status: body.status || 'active', billingStatus: body.billingStatus || 'normal' } };
|
||||
if (pathname === '/api/platform-admin/tenants/billing-profile') return { item: { ...body } };
|
||||
if (pathname === '/api/platform-admin/audit-logs') return { items: [{ id: 'audit-smoke', tenantId: ids.tenant, action: 'tenant.updated', targetType: 'tenant', targetId: ids.tenant, actorName: '平台管理员', createdAt: new Date().toISOString(), details: { source: 'smoke' } }] };
|
||||
if (pathname === '/api/platform-admin/invoices' && method === 'GET') return { items: [{ id: ids.invoice, tenantId: ids.tenant, tenantSlug: 'master', tenantName: '工学题库', invoiceNo: 'INV-SMOKE', invoiceType: 'subscription', status: 'unpaid', currency: 'CNY', totalCents: 199900, paidCents: 0, balanceCents: 199900, dueDate: '2026-07-10' }] };
|
||||
if (pathname === '/api/platform-admin/invoices/reminders') return { items: [{ id: 'reminder-smoke', tenantId: ids.tenant, tenantName: '工学题库', invoiceId: ids.invoice, invoiceNo: 'INV-SMOKE', reminderType: 'overdue', channel: 'internal', status: 'pending', reminderLevel: 1, balanceCentsSnapshot: 199900, message: '烟测催缴' }] };
|
||||
if (pathname === '/api/platform-admin/usage' && method === 'GET') return { items: [{ id: 'usage-smoke', tenantId: ids.tenant, tenantName: '工学题库', metricKey: 'students', metricValue: 3722, periodStart: '2026-07-01', periodEnd: '2026-07-31' }] };
|
||||
if (pathname === '/api/platform-admin/usage' && method === 'POST') return { item: { id: 'usage-created-smoke', ...body } };
|
||||
if (pathname === '/api/platform-admin/subscriptions') return { item: { id: 'subscription-created-smoke', ...body } };
|
||||
if (pathname === '/api/platform-admin/invoices/from-subscription') return { item: { id: ids.invoice, invoiceNo: 'INV-SMOKE', ...body } };
|
||||
if (pathname === '/api/platform-admin/invoices/subscription-candidates') return { items: [{ tenantId: ids.tenant, tenantSlug: 'master', tenantName: '工学题库', subscriptionId: 'subscription-smoke', planCode: 'starter_yearly', planName: '基础年费', status: 'active', amountCents: 199900, expiresAt: '2026-07-20', hasExistingInvoice: false, wouldCreate: true }] };
|
||||
if (pathname === '/api/platform-admin/invoices/from-subscriptions-batch') return { item: { dryRun: body.dryRun === true, createdCount: body.dryRun ? 0 : 1, skippedCount: 0, items: [{ tenantId: ids.tenant, subscriptionId: 'subscription-smoke', wouldCreate: true }] } };
|
||||
if (pathname === '/api/platform-admin/invoices/usage-overage-candidates') return { items: [{ tenantId: ids.tenant, tenantSlug: 'master', tenantName: '工学题库', planCode: 'starter_yearly', planName: '基础年费', periodStart: query.get('periodStart') || '2026-07-01', periodEnd: query.get('periodEnd') || '2026-07-31', hasExistingInvoice: false, wouldCreate: true, totalCents: 8800, items: [{ description: '超额学生数', quantity: 88, unitAmountCents: 100 }] }] };
|
||||
if (pathname === '/api/platform-admin/invoices/from-usage-overage') return { item: { dryRun: body.dryRun === true, createdCount: body.dryRun ? 0 : 1, skippedCount: 0, totalCents: 8800, items: [{ tenantId: ids.tenant, periodStart: body.periodStart, periodEnd: body.periodEnd, totalCents: 8800, wouldCreate: true }] } };
|
||||
if (pathname === '/api/platform-admin/invoices/payments/manual-confirm') return { item: { id: 'payment-smoke', invoiceId: body.invoiceId || ids.invoice, amountCents: body.amountCents, status: 'confirmed' } };
|
||||
if (pathname === '/api/platform-admin/invoices/process-overdue') return { item: { dryRun: body.dryRun === true, processed: 1, markedOverdue: body.dryRun ? 0 : 1, reminderCreated: body.dryRun ? 0 : 1, skippedReminder: 0, items: [{ id: ids.invoice, wouldMarkOverdue: true, wouldCreateReminder: true }] } };
|
||||
if (pathname === '/api/platform-admin/question-banks') return { items: [{ id: ids.publicBank, tenantId: null, name: '平台公共题库', regionId: ids.region, regionName: '天津', sourceScope: 'platform_public', status: 'active', questionCount: 128 }] };
|
||||
if (pathname === '/api/platform-admin/question-bank-grants' && method === 'GET') return { items: [{ id: ids.publicBankGrant, sourceQuestionBankId: ids.publicBank, sourceQuestionBankName: '平台公共题库', sourceRegionName: '天津', grantScope: 'all_active_tenants', allowedPlanCodes: ['starter_yearly'], status: 'active' }] };
|
||||
if (pathname === '/api/platform-admin/question-bank-grants' && method === 'PUT') return { item: { id: body.id || ids.publicBankGrant, ...body } };
|
||||
if (pathname === '/api/platform-admin/staff' && method === 'GET') return { items: [{ id: ids.platformStaff, name: '平台管理员', username: 'platform_admin', email: 'platform@example.test', phone: '13600000000', primaryRole: 'platform_admin', status: 'active', authUserId: 'auth-smoke', platformPermissions: { '*': true }, createdAt: new Date().toISOString() }] };
|
||||
if (pathname === '/api/platform-admin/staff' && method === 'PUT') return { item: { id: body.id || ids.platformStaff, ...body, primaryRole: 'platform_admin', platformPermissions: body.platformPermissions || { '*': true } } };
|
||||
if (pathname === '/api/platform-admin/staff/status') return { item: { id: body.staffId || ids.platformStaff, status: body.status || 'active' } };
|
||||
if (pathname.startsWith('/api/platform-admin/')) return method === 'GET' ? { items: [], item: null } : { ok: true, item: { id: 'platform-smoke' } };
|
||||
|
||||
return method === 'GET' ? { items: [], item: null } : { ok: true, item: { id: 'smoke' } };
|
||||
@@ -762,6 +1057,7 @@ class CdpPage {
|
||||
await this.send('Page.enable');
|
||||
await this.send('Log.enable');
|
||||
await this.send('Network.enable');
|
||||
await this.send('Page.setInterceptFileChooserDialog', { enabled: false }).catch(() => {});
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -795,6 +1091,12 @@ class CdpPage {
|
||||
await this.send('Page.navigate', { url });
|
||||
}
|
||||
|
||||
async mouseClick(x, y) {
|
||||
await this.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, button: 'none' });
|
||||
await this.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
||||
await this.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
||||
}
|
||||
|
||||
close() {
|
||||
this.ws.close();
|
||||
}
|
||||
@@ -851,6 +1153,14 @@ class CdpPage {
|
||||
.filter(Boolean)
|
||||
.slice(-20);
|
||||
}
|
||||
|
||||
async acceptDialogs() {
|
||||
const events = this.events.filter(event => event.method === 'Page.javascriptDialogOpening');
|
||||
this.events = this.events.filter(event => event.method !== 'Page.javascriptDialogOpening');
|
||||
for (const event of events) {
|
||||
await this.send('Page.handleJavaScriptDialog', { accept: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function newPage(browser, url) {
|
||||
@@ -907,6 +1217,7 @@ async function waitForPath(page, pathPart, timeoutMs = 10_000) {
|
||||
}
|
||||
|
||||
async function clickText(page, text) {
|
||||
await page.acceptDialogs();
|
||||
const result = await page.evaluate(`
|
||||
(() => {
|
||||
const expected = ${JSON.stringify(text)};
|
||||
@@ -923,6 +1234,7 @@ async function clickText(page, text) {
|
||||
'.admin-button',
|
||||
'.platform-row',
|
||||
'.platform-button',
|
||||
'.platform-chip',
|
||||
'[class*="button"]'
|
||||
].join(',');
|
||||
const visible = el => {
|
||||
@@ -959,20 +1271,327 @@ async function clickText(page, text) {
|
||||
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
|
||||
}
|
||||
target.click();
|
||||
return { ok: true, tag: target.tagName, className: target.className, text: textOf(target).slice(0, 120) };
|
||||
return { ok: true, tag: target.tagName, className: target.className, text: textOf(target).slice(0, 120), x, y };
|
||||
})()
|
||||
`);
|
||||
await page.acceptDialogs();
|
||||
if (!result?.ok) throw new Error(`Clickable text not found: ${text}\n${result?.body || ''}`);
|
||||
if (Number.isFinite(result.x) && Number.isFinite(result.y)) await page.mouseClick(result.x, result.y);
|
||||
await delay(350);
|
||||
await page.acceptDialogs();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function clickTextInSection(page, sectionTitle, text) {
|
||||
await page.acceptDialogs();
|
||||
const result = await page.evaluate(`
|
||||
(() => {
|
||||
const sectionTitle = ${JSON.stringify(sectionTitle)};
|
||||
const expected = ${JSON.stringify(text)};
|
||||
const clickableSelector = [
|
||||
'button',
|
||||
'taro-button-core',
|
||||
'a',
|
||||
'[role="button"]',
|
||||
'[onclick]',
|
||||
'.admin-row',
|
||||
'.admin-button',
|
||||
'.platform-row',
|
||||
'.platform-button',
|
||||
'.platform-chip',
|
||||
'[class*="button"]'
|
||||
].join(',');
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const normalized = value => String(value || '').replace(/\\s+/g, ' ').trim();
|
||||
const textOf = el => normalized(el.innerText || el.textContent || '');
|
||||
const sections = Array.from(document.querySelectorAll('.admin-section,.platform-section,section'))
|
||||
.filter(el => visible(el) && textOf(el).includes(sectionTitle));
|
||||
const root = sections.sort((a, b) => textOf(a).length - textOf(b).length)[0];
|
||||
if (!root) return { ok: false, body: (document.body?.innerText || '').slice(0, 1200) };
|
||||
const byElement = new Map();
|
||||
for (const el of Array.from(root.querySelectorAll('*'))) {
|
||||
if (!visible(el)) continue;
|
||||
const nodeText = textOf(el);
|
||||
if (!nodeText.includes(expected)) continue;
|
||||
const target = el.matches(clickableSelector) ? el : el.closest(clickableSelector);
|
||||
if (!target || !visible(target) || !root.contains(target)) continue;
|
||||
const targetText = textOf(target);
|
||||
if (!targetText.includes(expected)) continue;
|
||||
const exact = targetText === expected ? 0 : 1;
|
||||
const tagScore = ['BUTTON', 'TARO-BUTTON-CORE', 'A'].includes(target.tagName) ? 0 : 1;
|
||||
const lengthScore = targetText.length;
|
||||
const previous = byElement.get(target);
|
||||
const next = { el: target, text: targetText, score: exact * 10000 + tagScore * 1000 + lengthScore };
|
||||
if (!previous || next.score < previous.score) byElement.set(target, next);
|
||||
}
|
||||
const candidates = Array.from(byElement.values()).sort((a, b) => a.score - b.score);
|
||||
const target = candidates[0]?.el;
|
||||
if (!target) return { ok: false, body: textOf(root).slice(0, 1200) };
|
||||
target.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
const rect = target.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
||||
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
|
||||
}
|
||||
target.click();
|
||||
return { ok: true, tag: target.tagName, className: target.className, text: textOf(target).slice(0, 120), x, y };
|
||||
})()
|
||||
`);
|
||||
await page.acceptDialogs();
|
||||
if (!result?.ok) throw new Error(`Clickable text not found in section "${sectionTitle}": ${text}\n${result?.body || ''}`);
|
||||
if (Number.isFinite(result.x) && Number.isFinite(result.y)) await page.mouseClick(result.x, result.y);
|
||||
await delay(350);
|
||||
await page.acceptDialogs();
|
||||
return result;
|
||||
}
|
||||
|
||||
async function clickTextIfPresent(page, text) {
|
||||
const exists = await page.evaluate(`
|
||||
(() => {
|
||||
const expected = ${JSON.stringify(text)};
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
return Array.from(document.querySelectorAll('button,taro-button-core,a,[role="button"],[onclick],[class*="button"]'))
|
||||
.some(el => visible(el) && String(el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim().includes(expected));
|
||||
})()
|
||||
`);
|
||||
if (!exists) return false;
|
||||
await clickText(page, text);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function clickVisibleTextCandidate(page, texts) {
|
||||
const result = await page.evaluate(`
|
||||
(() => {
|
||||
const expectedTexts = ${JSON.stringify(texts)};
|
||||
const clickableSelector = [
|
||||
'button',
|
||||
'taro-button-core',
|
||||
'a',
|
||||
'[role="button"]',
|
||||
'[onclick]',
|
||||
'.platform-chip',
|
||||
'[class*="button"]',
|
||||
'[class*="btn"]',
|
||||
'[class*="model__btn"]',
|
||||
'[class*="model__confirm"]'
|
||||
].join(',');
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const normalized = value => String(value || '').replace(/\\s+/g, ' ').trim();
|
||||
const textOf = el => normalized(el.innerText || el.textContent || '');
|
||||
const modalSelector = [
|
||||
'.weui-dialog',
|
||||
'.weui-picker',
|
||||
'.taro-modal',
|
||||
'[role="dialog"]',
|
||||
'[aria-modal="true"]',
|
||||
'[class*="dialog"]',
|
||||
'[class*="modal"]'
|
||||
].join(',');
|
||||
const activeRoots = Array.from(document.querySelectorAll(modalSelector)).filter(visible);
|
||||
if (!activeRoots.length) activeRoots.push(document.body);
|
||||
const candidates = [];
|
||||
activeRoots.forEach((root, rootIndex) => {
|
||||
for (const el of Array.from(root.querySelectorAll('*'))) {
|
||||
if (!visible(el)) continue;
|
||||
const text = textOf(el);
|
||||
if (!expectedTexts.some(expected => text === expected || text.includes(expected))) continue;
|
||||
const target = el.matches(clickableSelector) ? el : el.closest(clickableSelector);
|
||||
if (!target || !visible(target) || !root.contains(target)) continue;
|
||||
const targetText = textOf(target);
|
||||
const order = expectedTexts.findIndex(expected => targetText === expected || targetText.includes(expected));
|
||||
const exact = expectedTexts.some(expected => targetText === expected) ? 0 : 1;
|
||||
candidates.push({ el: target, text: targetText, score: rootIndex * 1000000 + order * 100000 + exact * 10000 + targetText.length });
|
||||
}
|
||||
});
|
||||
candidates.sort((a, b) => a.score - b.score);
|
||||
const target = candidates[0]?.el;
|
||||
if (!target) return { ok: false, body: (document.body?.innerText || '').slice(0, 1200) };
|
||||
target.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
const rect = target.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
||||
target.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
|
||||
}
|
||||
target.click();
|
||||
return { ok: true, text: textOf(target).slice(0, 120), x, y };
|
||||
})()
|
||||
`);
|
||||
if (result?.ok) {
|
||||
if (Number.isFinite(result.x) && Number.isFinite(result.y)) await page.mouseClick(result.x, result.y);
|
||||
await delay(250);
|
||||
await page.acceptDialogs();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function clickTextAndConfirm(page, text) {
|
||||
await clickText(page, text);
|
||||
await delay(250);
|
||||
await page.acceptDialogs();
|
||||
await clickVisibleTextCandidate(page, ['确认', '发布', '停用', '禁用', '恢复', '重试', '忽略', '已打款']);
|
||||
await page.acceptDialogs();
|
||||
}
|
||||
|
||||
async function waitForApiRequest(api, path, method) {
|
||||
await waitUntil(`${method || ''} ${path} request`, async () => {
|
||||
return api.requests.some(item => item.path === path && (!method || item.method === method));
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
async function waitForApiRequestCount(api, path, method, minCount, timeoutMs = 10_000) {
|
||||
await waitUntil(`${method || ''} ${path} request count >= ${minCount}`, async () => {
|
||||
return api.requests.filter(item => item.path === path && (!method || item.method === method)).length >= minCount;
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
function requestCount(api, path, method) {
|
||||
return api.requests.filter(item => item.path === path && (!method || item.method === method)).length;
|
||||
}
|
||||
|
||||
async function clickTextAndConfirmForApi(page, api, text, path, method) {
|
||||
const before = requestCount(api, path, method);
|
||||
await clickText(page, text);
|
||||
const confirmTexts = ['确认', '发布', '停用', '禁用', '恢复', '重试', '忽略', '已打款'];
|
||||
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||
await page.acceptDialogs();
|
||||
const hasModal = await page.evaluate(`
|
||||
(() => {
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const modalSelector = '.weui-dialog,.weui-half-screen-dialog,.taro-modal__content,[role="dialog"],[aria-modal="true"],[class*="dialog"],[class*="modal__content"]';
|
||||
return Array.from(document.querySelectorAll(modalSelector)).some(visible);
|
||||
})()
|
||||
`);
|
||||
if (hasModal) await clickVisibleTextCandidate(page, confirmTexts);
|
||||
await page.acceptDialogs();
|
||||
try {
|
||||
await waitForApiRequestCount(api, path, method, before + 1, 1_200);
|
||||
return;
|
||||
} catch {
|
||||
await delay(250);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await waitForApiRequestCount(api, path, method, before + 1, 2_000);
|
||||
} catch (error) {
|
||||
const diagnostics = await page.evaluate(`
|
||||
(() => {
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const normalized = value => String(value || '').replace(/\\s+/g, ' ').trim();
|
||||
const modalSelector = '.weui-dialog,.weui-mask,.weui-half-screen-dialog,.taro-modal,[role="dialog"],[aria-modal="true"],[class*="dialog"],[class*="modal"]';
|
||||
return {
|
||||
body: normalized(document.body?.innerText || '').slice(0, 1500),
|
||||
modals: Array.from(document.querySelectorAll(modalSelector))
|
||||
.filter(visible)
|
||||
.map(el => ({ tag: el.tagName, className: String(el.className || ''), text: normalized(el.innerText || el.textContent || '').slice(0, 600) })),
|
||||
buttons: Array.from(document.querySelectorAll('button,taro-button-core,a,[role="button"],[onclick],[class*="button"],[class*="btn"]'))
|
||||
.filter(visible)
|
||||
.map(el => ({ tag: el.tagName, className: String(el.className || ''), text: normalized(el.innerText || el.textContent || '').slice(0, 120) }))
|
||||
.filter(item => item.text.includes('发布') || item.text.includes('确认') || item.text.includes('重试') || item.text.includes('停用') || item.text.includes('禁用') || item.text.includes('已打款'))
|
||||
.slice(0, 30)
|
||||
};
|
||||
})()
|
||||
`).catch(error => ({ error: error.message }));
|
||||
const recentRequests = api.requests.slice(-20).map(item => `${item.method} ${item.path}`);
|
||||
throw new Error(`${error.message}\nConfirm diagnostics: ${JSON.stringify(diagnostics, null, 2)}\nRecent API requests: ${JSON.stringify(recentRequests, null, 2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fillByPlaceholder(page, placeholder, value, occurrence = 0) {
|
||||
const result = await page.evaluate(`
|
||||
(() => {
|
||||
const expected = ${JSON.stringify(placeholder)};
|
||||
const nextValue = ${JSON.stringify(value)};
|
||||
const occurrence = ${Number(occurrence)};
|
||||
const elements = Array.from(document.querySelectorAll('input, textarea'))
|
||||
.filter(el => String(el.getAttribute('placeholder') || '').includes(expected));
|
||||
const target = elements[occurrence];
|
||||
if (!target) return { ok: false, body: (document.body?.innerText || '').slice(0, 1200) };
|
||||
target.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), 'value')?.set;
|
||||
if (setter) setter.call(target, nextValue);
|
||||
else target.value = nextValue;
|
||||
for (const type of ['input', 'change']) {
|
||||
target.dispatchEvent(new Event(type, { bubbles: true, cancelable: true }));
|
||||
}
|
||||
return { ok: true, placeholder: target.getAttribute('placeholder'), value: target.value };
|
||||
})()
|
||||
`);
|
||||
if (!result?.ok) throw new Error(`Input placeholder not found: ${placeholder}\n${result?.body || ''}`);
|
||||
await delay(150);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function fillByPlaceholderInSection(page, sectionTitle, placeholder, value, occurrence = 0) {
|
||||
const result = await page.evaluate(`
|
||||
(() => {
|
||||
const sectionTitle = ${JSON.stringify(sectionTitle)};
|
||||
const expected = ${JSON.stringify(placeholder)};
|
||||
const nextValue = ${JSON.stringify(value)};
|
||||
const occurrence = ${Number(occurrence)};
|
||||
const visible = el => {
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.visibility !== 'hidden' && style.display !== 'none' && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const normalized = value => String(value || '').replace(/\\s+/g, ' ').trim();
|
||||
const sections = Array.from(document.querySelectorAll('.admin-section,.platform-section,section'))
|
||||
.filter(el => visible(el) && normalized(el.innerText || el.textContent || '').includes(sectionTitle));
|
||||
const root = sections.sort((a, b) => normalized(a.innerText || a.textContent || '').length - normalized(b.innerText || b.textContent || '').length)[0];
|
||||
if (!root) return { ok: false, body: (document.body?.innerText || '').slice(0, 1200) };
|
||||
const elements = Array.from(root.querySelectorAll('input, textarea'))
|
||||
.filter(el => String(el.getAttribute('placeholder') || '').includes(expected));
|
||||
const target = elements[occurrence];
|
||||
if (!target) return { ok: false, body: normalized(root.innerText || root.textContent || '').slice(0, 1200) };
|
||||
target.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), 'value')?.set;
|
||||
if (setter) setter.call(target, nextValue);
|
||||
else target.value = nextValue;
|
||||
for (const type of ['input', 'change']) {
|
||||
target.dispatchEvent(new Event(type, { bubbles: true, cancelable: true }));
|
||||
}
|
||||
return { ok: true, placeholder: target.getAttribute('placeholder'), value: target.value };
|
||||
})()
|
||||
`);
|
||||
if (!result?.ok) throw new Error(`Input placeholder not found in section "${sectionTitle}": ${placeholder}\n${result?.body || ''}`);
|
||||
await delay(150);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function ensureInputValue(page, placeholder, value, occurrence = 0) {
|
||||
const current = await page.evaluate(`
|
||||
(() => {
|
||||
const expected = ${JSON.stringify(placeholder)};
|
||||
const occurrence = ${Number(occurrence)};
|
||||
const elements = Array.from(document.querySelectorAll('input, textarea'))
|
||||
.filter(el => String(el.getAttribute('placeholder') || '').includes(expected));
|
||||
return elements[occurrence]?.value || '';
|
||||
})()
|
||||
`);
|
||||
if (!current) await fillByPlaceholder(page, placeholder, value, occurrence);
|
||||
}
|
||||
|
||||
async function navigateAndExpect(page, baseUrl, path, text, timeoutMs = 10_000) {
|
||||
await page.navigate(`${baseUrl}${path}`);
|
||||
await waitForPath(page, path.split('?')[0], timeoutMs);
|
||||
@@ -1084,7 +1703,7 @@ async function runStudentJourney(browser, portal, api) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runTenantJourney(browser, portal) {
|
||||
async function runTenantJourney(browser, portal, api) {
|
||||
const checks = [];
|
||||
const page = await newPage(browser, `${portal.baseUrl}${portal.landingPath}`);
|
||||
try {
|
||||
@@ -1097,6 +1716,27 @@ async function runTenantJourney(browser, portal) {
|
||||
await waitForText(page, '题库内容');
|
||||
checks.push({ id: 'tenant.workbench.to_content', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
await fillByPlaceholder(page, '粘贴题目', '[{"title":"smoke","type":"single_choice"}]');
|
||||
await clickText(page, '后端预览');
|
||||
await waitForApiRequest(api, '/api/tenant-content/imports/preview/questions', 'POST');
|
||||
await waitForText(page, '预览第');
|
||||
await clickTextAndConfirm(page, '执行导入');
|
||||
await waitForApiRequest(api, '/api/tenant-content/imports/questions', 'POST');
|
||||
await clickTextAndConfirm(page, '复检任务');
|
||||
await waitForApiRequest(api, '/api/tenant-content/imports/post-check', 'POST');
|
||||
checks.push({ id: 'tenant.content.import_preview_execute_postcheck', status: 'pass', detail: '内容导入预览、执行和复检 API 已触发' });
|
||||
|
||||
await clickTextAndConfirm(page, '采纳');
|
||||
await waitForApiRequest(api, '/api/tenant-content/public-question-banks/adopt', 'POST');
|
||||
await clickTextAndConfirm(page, '同步');
|
||||
await waitForApiRequest(api, '/api/tenant-content/public-question-banks/sync', 'POST');
|
||||
await waitForText(page, '同步冲突');
|
||||
await clickTextAndConfirm(page, '全部保留本地');
|
||||
await waitForApiRequest(api, '/api/tenant-content/public-question-banks/conflicts/resolve-batch', 'POST');
|
||||
await clickText(page, '标记已读');
|
||||
await waitForApiRequest(api, '/api/tenant-content/notifications/status', 'POST');
|
||||
checks.push({ id: 'tenant.content.public_bank_adopt_sync_conflict', status: 'pass', detail: '公共题库采纳、同步、冲突处理和通知状态 API 已触发' });
|
||||
|
||||
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
|
||||
await waitForText(page, '财务运营');
|
||||
await clickText(page, '财务运营');
|
||||
@@ -1104,27 +1744,84 @@ async function runTenantJourney(browser, portal) {
|
||||
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) });
|
||||
}
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/tenant-admin/dashboard/index', '数据看板');
|
||||
checks.push({ id: 'tenant.workbench.to_dashboard', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/tenant-admin/students/index', '学生运营');
|
||||
await waitForText(page, '测试学生');
|
||||
await clickText(page, '详情');
|
||||
await fillByPlaceholder(page, '学生备注', 'H5 烟测备注');
|
||||
await clickText(page, '保存备注');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/notes', 'PUT');
|
||||
await fillByPlaceholder(page, '标题', 'H5 烟测跟进');
|
||||
await clickText(page, '保存跟进');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/followups', 'PUT');
|
||||
await clickText(page, '预览候选');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/supervision/preview', 'GET');
|
||||
await waitForText(page, '近期答题偏少');
|
||||
await clickText(page, '生成跟进任务');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/supervision/generate', 'POST');
|
||||
await clickText(page, '保存每日规则');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/supervision/rules', 'PUT');
|
||||
await fillByPlaceholder(page, '跟进标题', 'H5 烟测 CRM 推送');
|
||||
await fillByPlaceholder(page, '每行一个学生 userId', ids.user);
|
||||
await clickText(page, '入队推送');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/students/crm-push', 'POST');
|
||||
checks.push({ id: 'tenant.students.operations', status: 'pass', detail: '备注、跟进、督导预览/生成/规则和 CRM 推送 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/tenant-admin/marketing/index', '营销中心');
|
||||
await fillByPlaceholder(page, '任务编码', 'h5-smoke-task');
|
||||
await fillByPlaceholder(page, '任务名称', 'H5 烟测积分任务');
|
||||
await clickText(page, '保存积分任务');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/point-activity-tasks', 'PUT');
|
||||
await fillByPlaceholder(page, '兑换编码', 'h5-smoke-gift');
|
||||
await fillByPlaceholder(page, '兑换名称', 'H5 烟测兑换商品');
|
||||
await clickText(page, '保存兑换商品');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/point-exchange-items', 'PUT');
|
||||
await fillByPlaceholder(page, '优惠券码', 'SMOKE90');
|
||||
await fillByPlaceholder(page, '优惠金额', '9');
|
||||
await clickText(page, '保存优惠券');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/coupons', 'PUT');
|
||||
await clickText(page, '保存 CRM');
|
||||
await waitForApiRequest(api, '/api/crm/config', 'PUT');
|
||||
await clickText(page, '日志');
|
||||
await waitForApiRequest(api, '/api/crm/queue/logs', 'GET');
|
||||
await clickTextAndConfirmForApi(page, api, '重试', '/api/crm/queue/action', 'POST');
|
||||
await clickText(page, '保存规则');
|
||||
await waitForApiRequest(api, '/api/commission/settings', 'PUT');
|
||||
await clickTextInSection(page, '成员分佣比例', '测试销售');
|
||||
await clickText(page, '保存成员比例');
|
||||
await waitForApiRequest(api, '/api/commission/member-rate', 'PUT');
|
||||
await clickTextInSection(page, '分佣统计', '测试销售');
|
||||
await clickText(page, '生成结算单');
|
||||
await waitForApiRequest(api, '/api/commission/settlements/generate', 'POST');
|
||||
await clickText(page, '导出 CSV');
|
||||
await waitForApiRequest(api, '/api/commission/settlements/export', 'GET');
|
||||
checks.push({ id: 'tenant.marketing.operations', status: 'pass', detail: '积分、兑换、优惠券、CRM、分佣规则、成员比例和结算 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/tenant-admin/settings/index', '租户设置');
|
||||
await clickText(page, '保存草稿');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/theme/preview', 'POST');
|
||||
await waitForText(page, '草稿:专注主题');
|
||||
await clickTextAndConfirmForApi(page, api, '发布主题', '/api/tenant-admin/theme/publish', 'POST');
|
||||
await clickText(page, '新建模板');
|
||||
await fillByPlaceholder(page, '模板名称', 'H5 烟测角色');
|
||||
await fillByPlaceholder(page, '模板编码', 'h5-smoke-role');
|
||||
await clickText(page, '保存模板');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/role-templates', 'PUT');
|
||||
await clickText(page, '新建成员');
|
||||
await fillByPlaceholder(page, '姓名', 'H5 烟测成员', 1);
|
||||
await fillByPlaceholder(page, '手机号', '13911112222', 1);
|
||||
await clickText(page, '保存成员');
|
||||
await waitForApiRequest(api, '/api/tenant-admin/members', 'PUT');
|
||||
checks.push({ id: 'tenant.settings.brand_role_member', status: 'pass', detail: '主题草稿/发布、角色模板和成员绑定 API 已触发' });
|
||||
return checks;
|
||||
} finally {
|
||||
page.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function runPlatformJourney(browser, portal) {
|
||||
async function runPlatformJourney(browser, portal, api) {
|
||||
const checks = [];
|
||||
const page = await newPage(browser, `${portal.baseUrl}${portal.landingPath}`);
|
||||
try {
|
||||
@@ -1137,6 +1834,18 @@ async function runPlatformJourney(browser, portal) {
|
||||
await waitForText(page, '租户管理');
|
||||
checks.push({ id: 'platform.workbench.to_tenants', status: 'pass', detail: await currentPath(page) });
|
||||
|
||||
await fillByPlaceholder(page, 'tenant-slug', 'smoke-tenant');
|
||||
await fillByPlaceholder(page, '合作商名称', '烟测合作商');
|
||||
await clickTextAndConfirmForApi(page, api, '创建租户', '/api/platform-admin/tenants', 'POST');
|
||||
await waitForApiRequest(api, '/api/platform-admin/tenants/detail', 'GET');
|
||||
await waitForText(page, '租户详情');
|
||||
await clickTextInSection(page, '租户列表', '详情');
|
||||
await waitForText(page, ids.tenant);
|
||||
await clickTextAndConfirmForApi(page, api, '提交状态变更', '/api/platform-admin/tenants/status', 'PATCH');
|
||||
await fillByPlaceholder(page, '公司或个人抬头', '烟测合作商有限公司');
|
||||
await clickTextAndConfirmForApi(page, api, '保存账务资料', '/api/platform-admin/tenants/billing-profile', 'PUT');
|
||||
checks.push({ id: 'platform.tenants.create_status_billing', status: 'pass', detail: '平台租户创建、状态变更和账务资料 API 已触发' });
|
||||
|
||||
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
|
||||
await waitForText(page, '账务中心');
|
||||
await clickText(page, '账务中心');
|
||||
@@ -1144,18 +1853,50 @@ async function runPlatformJourney(browser, portal) {
|
||||
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) });
|
||||
}
|
||||
await ensureInputValue(page, 'tenantId', ids.tenant, 0);
|
||||
await ensureInputValue(page, 'starter_yearly', 'starter_yearly');
|
||||
await clickTextAndConfirm(page, '开通订阅');
|
||||
await waitForApiRequest(api, '/api/platform-admin/subscriptions', 'POST');
|
||||
await ensureInputValue(page, 'tenantId', ids.tenant, 1);
|
||||
await clickTextAndConfirm(page, '生成订阅账单');
|
||||
await waitForApiRequest(api, '/api/platform-admin/invoices/from-subscription', 'POST');
|
||||
await fillByPlaceholderInSection(page, '收款与用量', 'tenantId', ids.tenant, 0);
|
||||
await fillByPlaceholderInSection(page, '收款与用量', 'invoiceId', ids.invoice);
|
||||
await fillByPlaceholderInSection(page, '收款与用量', '元', '1999');
|
||||
await clickTextAndConfirmForApi(page, api, '确认收款', '/api/platform-admin/invoices/payments/manual-confirm', 'POST');
|
||||
await fillByPlaceholderInSection(page, '收款与用量', 'tenantId', ids.tenant, 1);
|
||||
await fillByPlaceholderInSection(page, '收款与用量', '0', '3722');
|
||||
await clickTextAndConfirmForApi(page, api, '记录用量', '/api/platform-admin/usage', 'POST');
|
||||
await clickText(page, '预览批量');
|
||||
await waitForApiRequest(api, '/api/platform-admin/invoices/from-subscriptions-batch', 'POST');
|
||||
await clickTextAndConfirmForApi(page, api, '批量生成账单', '/api/platform-admin/invoices/from-subscriptions-batch', 'POST');
|
||||
await clickText(page, '预览超额');
|
||||
await waitForApiRequest(api, '/api/platform-admin/invoices/from-usage-overage', 'POST');
|
||||
await clickTextAndConfirmForApi(page, api, '生成超额账单', '/api/platform-admin/invoices/from-usage-overage', 'POST');
|
||||
await clickText(page, '预览逾期');
|
||||
await waitForApiRequest(api, '/api/platform-admin/invoices/process-overdue', 'POST');
|
||||
await clickTextAndConfirmForApi(page, api, '生成催缴', '/api/platform-admin/invoices/process-overdue', 'POST');
|
||||
checks.push({ id: 'platform.billing.operations', status: 'pass', detail: '订阅、账单、收款、用量、批量、超额和催缴 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/platform-admin/question-banks/index', '公共题库授权');
|
||||
checks.push({ id: 'platform.workbench.to_question-banks', status: 'pass', detail: await currentPath(page) });
|
||||
await clickText(page, '选择题库');
|
||||
await fillByPlaceholder(page, 'starter_yearly,regional_yearly', 'starter_yearly');
|
||||
await clickTextAndConfirmForApi(page, api, '保存授权', '/api/platform-admin/question-bank-grants', 'PUT');
|
||||
checks.push({ id: 'platform.question_banks.grant', status: 'pass', detail: '公共题库授权保存 API 已触发' });
|
||||
|
||||
await navigateAndExpect(page, portal.baseUrl, '/pages/platform-admin/staff/index', '平台员工');
|
||||
checks.push({ id: 'platform.workbench.to_staff', status: 'pass', detail: await currentPath(page) });
|
||||
await clickText(page, '清空表单');
|
||||
await fillByPlaceholder(page, 'auth.users.id', 'auth-h5-smoke');
|
||||
await fillByPlaceholder(page, 'platform_operator', 'h5_smoke_operator');
|
||||
await fillByPlaceholder(page, '员工姓名', 'H5 烟测平台员工');
|
||||
await waitForText(page, '员工编辑');
|
||||
await clickText(page, '员工编辑');
|
||||
await waitForText(page, '当前权限点\n1');
|
||||
await clickTextAndConfirmForApi(page, api, '保存员工', '/api/platform-admin/staff', 'PUT');
|
||||
await clickTextAndConfirmForApi(page, api, '禁用', '/api/platform-admin/staff/status', 'PATCH');
|
||||
checks.push({ id: 'platform.staff.operations', status: 'pass', detail: '平台员工保存和禁用 API 已触发' });
|
||||
return checks;
|
||||
} finally {
|
||||
page.close();
|
||||
@@ -1203,8 +1944,8 @@ async function main() {
|
||||
browser = await startBrowser(options);
|
||||
const checks = [];
|
||||
checks.push(...await runStudentJourney(browser, staticServers.find(item => item.portal === 'student'), api));
|
||||
checks.push(...await runTenantJourney(browser, staticServers.find(item => item.portal === 'tenant-admin')));
|
||||
checks.push(...await runPlatformJourney(browser, staticServers.find(item => item.portal === 'platform-admin')));
|
||||
checks.push(...await runTenantJourney(browser, staticServers.find(item => item.portal === 'tenant-admin'), api));
|
||||
checks.push(...await runPlatformJourney(browser, staticServers.find(item => item.portal === 'platform-admin'), api));
|
||||
|
||||
const payload = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
@@ -1226,6 +1967,11 @@ async function main() {
|
||||
paymentsCreated: api.requests.filter(item => item.path === '/api/commerce/payments/create' && item.method === 'POST').length,
|
||||
orderStatus: api.requests.filter(item => item.path === '/api/commerce/orders/status' && item.method === 'GET').length,
|
||||
tenantResolve: api.requests.filter(item => item.path === '/api/tenant/resolve').length,
|
||||
tenantContentWrites: api.requests.filter(item => item.path.startsWith('/api/tenant-content/') && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method)).length,
|
||||
tenantAdminWrites: api.requests.filter(item => item.path.startsWith('/api/tenant-admin/') && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method)).length,
|
||||
crmWrites: api.requests.filter(item => item.path.startsWith('/api/crm/') && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method)).length,
|
||||
commissionWrites: api.requests.filter(item => item.path.startsWith('/api/commission/') && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method)).length,
|
||||
platformAdminWrites: api.requests.filter(item => item.path.startsWith('/api/platform-admin/') && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(item.method)).length,
|
||||
},
|
||||
},
|
||||
checks,
|
||||
|
||||
Reference in New Issue
Block a user