feat(P2): 实现质量保障类5项功能(无障碍/视觉回归/通知渠道/漏洞扫描/灾备)

## 新增功能

### 1. 屏幕阅读器兼容性增强(a11y)
- 无障碍工具库:src/shared/lib/a11y.ts
- aria-live Hook:src/shared/hooks/use-aria-live.ts
- a11y 组件:skip-link/visually-hidden/focus-trap/aria-status
- 增强 UI:table.tsx 系统性 ARIA role,dialog.tsx aria-modal
- 审计文档:docs/accessibility/a11y-audit.md(WCAG 2.1 AA 清单)

### 2. 视觉回归测试
- 测试套件:tests/visual/(homepage + 3 个 dashboard)
- 3 视口(desktop/tablet/mobile)× 2 主题(light/dark)
- 动态元素遮罩,避免误报
- playwright.config.ts 新增 visual-chromium 项目
- 文档:docs/testing/visual-regression.md

### 3. 短信/微信推送渠道集成
- 新模块:src/modules/notifications/
- 4 个渠道:SMS(阿里云/腾讯云)、WeChat(公众号)、Email(SMTP)、In-App
- 分发器按用户偏好并行多渠道发送
- 外部 SDK 动态 import,Mock 模式开发可用
- 文档:docs/notifications/channels.md

### 4. 漏洞扫描 CI 集成
- CI security-scan job:npm audit + Snyk + Trivy FS + OWASP ZAP
- 独立工作流 security.yml:每周一深度扫描 + 容器镜像扫描
- 配置:suppressions.json + .trivyignore
- 本地脚本:security-scan.sh/ps1
- 文档:docs/security/scanning.md(SLA 分级)

### 5. 灾备方案
- 脚本:backup-verify/backup-offsite-sync/dr-drill/failover/health-check
- CI 增强:备份后校验+异地同步,每周灾备演练
- 独立工作流 dr-drill.yml:每周一凌晨 4 点自动演练
- 文档:docs/dr/dr-plan.md(RTO 4h/RPO 24h)+ dr-runbook.md(6 故障场景)

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
This commit is contained in:
SpecialX
2026-06-17 20:18:29 +08:00
parent b86255f0ea
commit 6585e10c6f
53 changed files with 7491 additions and 37 deletions

View File

@@ -283,6 +283,38 @@
"purpose": "生成导入模板 Buffer表头加粗+第二行填写说明+示例行)",
"deps": ["exceljs"],
"usedBy": ["users/import-export.generateUserImportTemplate"]
},
{
"name": "useA11yId",
"file": "lib/a11y.ts",
"signature": "useA11yId(prefix: string): string",
"purpose": "基于React.useId生成SSR安全的唯一ID用于aria-describedby、aria-labelledby等",
"deps": ["react"],
"usedBy": ["待扩展表单组件、a11y组件"]
},
{
"name": "mergeA11yProps",
"file": "lib/a11y.ts",
"signature": "mergeA11yProps<T extends Record<string, unknown>>(...props: (T | undefined | null | false)[]): T",
"purpose": "合并多组aria/data属性普通属性后者覆盖前者aria-*/data-*字符串属性以空格拼接",
"deps": [],
"usedBy": ["待扩展"]
},
{
"name": "describeInput",
"file": "lib/a11y.ts",
"signature": "describeInput(describedBy?: string, error?: string, hint?: string): { ariaDescribedBy?: string; ariaInvalid?: boolean }",
"purpose": "计算输入框的aria-describedby合并多个ID与aria-invaliderror存在则为true",
"deps": [],
"usedBy": ["待扩展(表单组件)"]
},
{
"name": "loadingAria",
"file": "lib/a11y.ts",
"signature": "loadingAria(isLoading: boolean): { ariaBusy: boolean; ariaLive: 'polite' | 'assertive' }",
"purpose": "提供加载状态的aria-busy与aria-live=polite属性",
"deps": [],
"usedBy": ["待扩展"]
}
],
"hooks": [
@@ -1318,6 +1350,40 @@
]
}
},
"notifications": {
"path": "src/modules/notifications",
"description": "通知渠道集成层基于用户通知偏好notification_preferences将通知分发到站内消息/SMS/微信公众号/邮件多渠道。所有渠道实现统一 NotificationChannelSender 接口dispatcher 按偏好并行发送。支持 Mock 模式(开发环境无需外部服务)。",
"exports": {
"actions": [
{ "name": "sendNotificationAction", "permission": "MESSAGE_SEND", "signature": "(payload: NotificationPayload) => Promise<ActionState<ChannelSendResult[]>>", "purpose": "发送通知给指定用户(按偏好多渠道分发)", "deps": ["requirePermission", "dispatcher.sendNotification"], "usedBy": ["待扩展"] },
{ "name": "sendClassNotificationAction", "permission": "MESSAGE_SEND", "signature": "(classId: string, payload: Omit<NotificationPayload, 'userId'>) => Promise<ActionState<ChannelSendResult[][]>>", "purpose": "发送班级通知(批量发送给班级所有学生;教师只能给自己所教班级发送)", "deps": ["requirePermission", "db.schema.classEnrollments", "db.schema.classes", "dispatcher.sendBatchNotifications"], "usedBy": ["待扩展"] }
],
"dispatcher": [
{ "name": "sendNotification", "signature": "(payload: NotificationPayload) => Promise<ChannelSendResult[]>", "file": "dispatcher.ts", "purpose": "发送单条通知:读取用户偏好+联系方式,按偏好选择渠道并行发送,记录日志", "deps": ["data-access.getUserNotificationPreferences", "data-access.getUserContactInfo", "data-access.logNotificationSendBatch", "channels.sms-channel.createSmsSender", "channels.wechat-channel.createWechatSender", "channels.email-channel.createEmailSender", "channels.in-app-channel.createInAppSender"], "usedBy": ["sendNotificationAction", "sendClassNotificationAction"] },
{ "name": "sendBatchNotifications", "signature": "(payloads: NotificationPayload[]) => Promise<ChannelSendResult[][]>", "file": "dispatcher.ts", "purpose": "批量发送通知(每个用户独立选择渠道,并行发送)", "deps": ["sendNotification"], "usedBy": ["sendClassNotificationAction"] }
],
"dataAccess": [
{ "name": "getUserNotificationPreferences", "signature": "(userId: string) => Promise<NotificationPreferences>", "file": "data-access.ts", "purpose": "获取用户通知偏好(复用 messaging.notification-preferences.getNotificationPreferences", "deps": ["messaging.notification-preferences.getNotificationPreferences"], "usedBy": ["dispatcher.sendNotification"] },
{ "name": "getUserContactInfo", "signature": "(userId: string) => Promise<ChannelRecipient>", "file": "data-access.ts", "purpose": "获取用户联系方式phone/emailwechatOpenId 暂不支持users 表无此字段)", "deps": ["shared.db", "shared.db.schema.users", "react.cache"], "usedBy": ["dispatcher.sendNotification"] },
{ "name": "logNotificationSend", "signature": "(result: ChannelSendResult) => void", "file": "data-access.ts", "purpose": "记录单条发送日志(当前使用 console.info未来可扩展 notification_logs 表)", "deps": [], "usedBy": ["logNotificationSendBatch"] },
{ "name": "logNotificationSendBatch", "signature": "(results: ChannelSendResult[]) => void", "file": "data-access.ts", "purpose": "批量记录发送日志", "deps": ["logNotificationSend"], "usedBy": ["dispatcher.sendNotification", "dispatcher.sendBatchNotifications"] }
],
"channels": [
{ "name": "createSmsSender", "file": "channels/sms-channel.ts", "purpose": "创建 SMS 渠道发送器aliyun/tencent/mock根据 SMS_PROVIDER 环境变量选择SDK 动态 import", "deps": ["环境变量: SMS_PROVIDER, SMS_ACCESS_KEY_ID, SMS_ACCESS_KEY_SECRET, SMS_SIGN_NAME, SMS_TEMPLATE_CODE"] },
{ "name": "createWechatSender", "file": "channels/wechat-channel.ts", "purpose": "创建微信渠道发送器(配置完整用真实发送器,否则 Mockaccess_token 带缓存)", "deps": ["环境变量: WECHAT_APP_ID, WECHAT_APP_SECRET, WECHAT_TEMPLATE_ID"] },
{ "name": "createEmailSender", "file": "channels/email-channel.ts", "purpose": "创建邮件渠道发送器(配置 EMAIL_HOST 用 Nodemailer SMTP否则 MockHTML 模板按 type 着色)", "deps": ["环境变量: EMAIL_HOST, EMAIL_PORT, EMAIL_USER, EMAIL_PASS, EMAIL_FROM"] },
{ "name": "createInAppSender", "file": "channels/in-app-channel.ts", "purpose": "创建站内消息渠道发送器(复用 messaging.data-access.createNotification 写入 message_notifications 表;总是启用)", "deps": ["messaging.data-access.createNotification"] }
],
"types": [
{ "name": "NotificationChannel", "type": "type", "file": "types.ts", "definition": "'in_app' | 'email' | 'sms' | 'wechat'", "usedBy": ["所有渠道文件", "dispatcher"] },
{ "name": "NotificationPayload", "type": "interface", "file": "types.ts", "definition": "{ userId, title, content, type: 'info'|'warning'|'error'|'success', metadata?, actionUrl? }", "usedBy": ["dispatcher", "actions", "所有渠道"] },
{ "name": "ChannelSendResult", "type": "interface", "file": "types.ts", "definition": "{ channel, success, messageId?, error?, sentAt }", "usedBy": ["dispatcher", "actions", "所有渠道"] },
{ "name": "NotificationChannelConfig", "type": "interface", "file": "types.ts", "definition": "{ enabled, sms?, wechat?, email? }", "usedBy": ["类型定义"] },
{ "name": "NotificationChannelSender", "type": "interface", "file": "channels/types.ts", "definition": "{ channel: NotificationChannel, send(payload, recipient), sendBatch(items) }", "usedBy": ["所有渠道实现", "dispatcher"] },
{ "name": "ChannelRecipient", "type": "interface", "file": "channels/types.ts", "definition": "{ userId, phone?, email?, wechatOpenId? }", "usedBy": ["所有渠道", "data-access.getUserContactInfo"] }
]
}
},
"attendance": {
"path": "src/modules/attendance",
"description": "学生考勤管理:教师按班级/日期点名(单条/批量)、查询考勤记录、统计出勤率/迟到率,学生/家长查看本人/子女考勤汇总,管理员查看全校考勤记录。支持班级考勤规则配置。",
@@ -1610,6 +1676,7 @@
"grades": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "types.permissions", "types.action-state", "db.schema.gradeRecords", "db.schema.classes", "db.schema.classEnrollments", "db.schema.subjects", "db.schema.users", "lib.excel"], "auth": ["auth"]}},
"parent": {"dependsOn": ["shared", "auth", "homework", "classes", "grades"], "uses": {"shared": ["db", "auth-guard.requireAuth", "db.schema.parentStudentRelations", "db.schema.users", "db.schema.grades", "db.schema.classEnrollments", "db.schema.classes", "types"], "auth": ["auth"], "homework": ["data-access.getStudentHomeworkAssignments", "data-access.getStudentDashboardGrades"], "classes": ["data-access.getStudentClasses", "data-access.getStudentSchedule"], "grades": ["data-access.getStudentGradeSummary"]}},
"messaging": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.requireAuth", "db.schema.messages", "db.schema.messageNotifications", "db.schema.notificationPreferences", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "db.schema.grades", "types.permissions", "types.action-state"], "auth": ["auth"]}},
"notifications": {"dependsOn": ["shared", "auth", "messaging"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.users", "db.schema.classEnrollments", "db.schema.classes", "types.permissions", "types.action-state"], "auth": ["auth"], "messaging": ["notification-preferences.getNotificationPreferences", "data-access.createNotification"]}},
"attendance": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "db.schema.attendanceRecords", "db.schema.attendanceRules", "db.schema.classEnrollments", "db.schema.users", "db.schema.classes", "types.permissions", "types.action-state", "types.DataScope"], "auth": ["auth"], "classes": ["data-access.getTeacherClasses", "data-access.getAdminClasses"]}},
"scheduling": {"dependsOn": ["shared", "auth", "classes"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.schedulingRules", "db.schema.scheduleChanges", "db.schema.classSchedule", "db.schema.classes", "db.schema.users", "db.schema.classSubjectTeachers", "db.schema.subjects", "db.schema.classrooms", "types.permissions", "types.action-state"], "auth": ["auth"], "classes": []}},
"diagnostic": {"dependsOn": ["shared", "auth"], "uses": {"shared": ["db", "auth-guard.requirePermission", "auth-guard.getAuthContext", "db.schema.knowledgePointMastery", "db.schema.learningDiagnosticReports", "db.schema.knowledgePoints", "db.schema.questionsToKnowledgePoints", "db.schema.examSubmissions", "db.schema.submissionAnswers", "db.schema.classEnrollments", "db.schema.classes", "db.schema.users", "types.permissions", "types.action-state", "hooks.usePermission", "components.ui.*"], "auth": ["auth"]}},
@@ -1823,19 +1890,74 @@
"trigger": "push/PR to main",
"steps": ["checkout", "cache npm", "configure npm proxy", "npm ci", "lint", "typecheck", "install playwright chromium", "integration tests", "e2e tests", "cache next.js build", "build", "prepare standalone", "deploy to docker"]
},
"security-audit": {
"security-scan": {
"runsOn": "ubuntu-latest",
"trigger": "push/PR to main",
"steps": ["checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate (continue-on-error)", "npm audit --audit-level=critical", "upload audit-report.json artifact"]
"needs": "build-deploy",
"continueOnError": true,
"steps": ["checkout", "setup node 20", "npm ci", "npm audit --audit-level=moderate + 生成 audit-report.json (continue-on-error)", "Snyk scan --severity-threshold=high --sarif-file-output=snyk.sarif (env SNYK_TOKEN, continue-on-error)", "Trivy fs scan json+table (continue-on-error)", "OWASP ZAP baseline scan target=NEXTAUTH_URL||localhost:8015 cmd_options='-a -j' (continue-on-error)", "upload security-reports artifact (audit-report.json, trivy-fs-report.json, snyk.sarif)"]
},
"scheduled-backup": {
"runsOn": "ubuntu-latest",
"trigger": "schedule cron 0 2 * * *",
"condition": "github.event_name == 'schedule'",
"steps": ["checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "upload backups/ artifact (retention 30 days)"]
"steps": ["checkout", "run scripts/backup-db.sh (env DATABASE_URL, BACKUP_DIR)", "run scripts/backup-verify.sh (校验备份完整性)", "run scripts/backup-offsite-sync.sh (异地同步, env BACKUP_OFFSITE_*, 失败不阻塞)", "upload backups/ artifact (retention 30 days)"]
},
"backup-verify": {
"runsOn": "ubuntu-latest",
"trigger": "schedule",
"condition": "github.event_name == 'schedule'",
"needs": "scheduled-backup",
"steps": ["checkout", "download db-backup artifact", "run scripts/backup-verify.sh (独立校验)", "run scripts/health-check.sh > health-report.json", "upload backup-verify-report artifact (backups/, health-report.json, retention 7 days)"]
},
"weekly-dr-drill": {
"runsOn": "ubuntu-latest",
"trigger": "schedule (每周触发, github.run_attempt % 7 == 0)",
"condition": "github.event_name == 'schedule' && github.run_attempt % 7 == 0",
"needs": "backup-verify",
"steps": ["checkout", "run scripts/dr-drill.sh (env DATABASE_URL, DR_DRILL_TEST_DB=next_edu_dr_drill)", "upload dr-drill-report artifact (docs/dr/reports/, retention 90 days)"]
}
}
},
"drDrillWorkflow": {
"configFile": ".gitea/workflows/dr-drill.yml",
"triggers": ["schedule cron 0 4 * * 1 (每周一凌晨 4 点)", "workflow_dispatch (inputs: backup_file, no_cleanup)"],
"job": "dr-drill",
"runsOn": "ubuntu-latest",
"timeoutMinutes": 30,
"steps": [
"checkout",
"install mysql-client",
"prepare backup directory (mkdir backups docs/dr/reports)",
"download db-backup artifact (continue-on-error) 或现场执行 backup-db.sh",
"run scripts/dr-drill.sh (支持 --backup/--no-cleanup 参数)",
"upload dr-drill-report-${{ github.run_id }} artifact (docs/dr/reports/, retention 90 days)",
"on failure: webhook 通知运维团队 (DR_NOTIFICATION_WEBHOOK)"
]
},
"securityWorkflow": {
"configFile": ".gitea/workflows/security.yml",
"triggers": ["schedule cron 0 3 * * 1 (每周一凌晨 3 点)", "workflow_dispatch (inputs: target_url, skip_dast)"],
"job": "deep-security-scan",
"runsOn": "ubuntu-latest",
"continueOnError": true,
"steps": [
"checkout",
"setup node 20",
"npm ci",
"npm audit + 生成 audit-report.json (依赖扫描)",
"Snyk scan --severity-threshold=medium --sarif-file-output=snyk.sarif (env SNYK_TOKEN, 深度依赖+静态分析)",
"Trivy fs scan json+table (文件系统扫描, trivy-fs-report.json)",
"Build Next.js standalone + docker build nextjs-app:scan + Trivy image scan (容器镜像扫描, trivy-image-report.json)",
"OWASP ZAP baseline scan (DAST, target=inputs.target_url||NEXTAUTH_URL||localhost:8015, 可通过 skip_dast 跳过)",
"Generate security-summary.md (jq 汇总各报告漏洞计数)",
"upload security-reports-full artifact (audit-report.json, trivy-fs-report.json, trivy-image-report.json, snyk.sarif, security-summary.md)"
],
"configFiles": {
"suppressions": ".gitea/suppressions.json (Snyk 漏洞抑制, 每条含 id/package/severity/reason/expires/owner)",
"trivyignore": ".trivyignore (Trivy CVE 忽略列表, 每行一个 CVE 带注释)"
}
},
"scripts": {
"scripts/audit.sh": {
"type": "bash",
@@ -1862,22 +1984,93 @@
"scripts/test-backup.sh": {
"type": "bash",
"purpose": "备份流程测试,执行一次备份并验证最新备份文件"
},
"scripts/backup-verify.sh": {
"type": "bash",
"purpose": "备份完整性校验:检查文件存在/大小/gzip 完整性/SQL 内容结构/SQL 语法(可选,需 DATABASE_URL",
"env": ["BACKUP_DIR", "DATABASE_URL", "BACKUP_VERIFY_MIN_SIZE"],
"exitCodes": {"0": "校验通过", "1": "校验失败"},
"options": ["--min-size BYTES", "--no-sql-check", "--help"]
},
"scripts/backup-offsite-sync.sh": {
"type": "bash",
"purpose": "异地备份同步:支持 S3/OSS/NFS 后端,同步后校验文件数量,清理远程过期备份(保留 90 天)",
"env": ["BACKUP_DIR", "BACKUP_OFFSITE_BACKEND", "BACKUP_OFFSITE_REMOTE", "BACKUP_OFFSITE_BUCKET", "BACKUP_OFFSITE_ACCESS_KEY", "BACKUP_OFFSITE_SECRET_KEY", "BACKUP_OFFSITE_REGION", "BACKUP_OFFSITE_RETENTION_DAYS"],
"tools": ["aws-cli (s3)", "rclone (s3/oss)", "ossutil (oss)", "rsync (nfs)"],
"exitCodes": {"0": "同步成功", "1": "同步失败"},
"options": ["--backend TYPE", "--no-cleanup", "--no-verify", "--help"]
},
"scripts/dr-drill.sh": {
"type": "bash",
"purpose": "灾备演练:创建测试库→从备份恢复→数据完整性检查→冒烟测试→清理→生成报告到 docs/dr/reports/",
"env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"],
"exitCodes": {"0": "演练成功", "1": "演练失败"},
"options": ["--backup FILE", "--test-db NAME", "--no-cleanup", "--report-dir DIR", "--help"]
},
"scripts/dr-drill.ps1": {
"type": "powershell",
"purpose": "灾备演练Windows PowerShell 5.1+ 版本),功能同 Bash 版本",
"env": ["DATABASE_URL", "BACKUP_DIR", "DR_DRILL_TEST_DB", "DR_DRILL_REPORT_DIR"],
"platform": "Windows",
"options": ["-BackupFile FILE", "-TestDb NAME", "-NoCleanup", "-ReportDir DIR", "-Help"]
},
"scripts/failover.sh": {
"type": "bash",
"purpose": "故障切换:检测主库健康→提升备库→更新应用配置→重启应用→验证切换",
"env": ["DATABASE_URL", "DATABASE_URL_STANDBY", "FAILOVER_APP_URL", "FAILOVER_APP_NAME", "FAILOVER_CONFIG_FILE", "FAILOVER_LOG_FILE"],
"exitCodes": {"0": "切换成功", "1": "切换失败"},
"options": ["--auto", "--primary URL", "--standby URL", "--app-url URL", "--no-restart", "--dry-run", "--help"]
},
"scripts/health-check.sh": {
"type": "bash",
"purpose": "健康检查:检查应用 HTTP/数据库连接/磁盘空间/备份新鲜度,输出 JSON 报告",
"env": ["DATABASE_URL", "HEALTH_CHECK_URL", "BACKUP_DIR", "HEALTH_CHECK_DISK_THRESHOLD", "HEALTH_CHECK_BACKUP_MAX_AGE"],
"exitCodes": {"0": "健康", "1": "异常"},
"options": ["--app-url URL", "--no-app", "--no-db", "--no-disk", "--no-backup", "--disk-threshold PCT", "--backup-max-age HRS", "--help"]
}
},
"packageJsonScripts": {
"audit": "npm audit --audit-level=moderate",
"audit:report": "npm audit --json > audit-report.json",
"security:audit": "npm audit --audit-level=moderate",
"security:scan": "bash scripts/security-scan.sh",
"backup": "bash scripts/backup-db.sh",
"restore": "bash scripts/restore-db.sh"
"restore": "bash scripts/restore-db.sh",
"dr:backup-verify": "bash scripts/backup-verify.sh",
"dr:offsite-sync": "bash scripts/backup-offsite-sync.sh",
"dr:drill": "bash scripts/dr-drill.sh",
"dr:drill:ps1": "powershell -ExecutionPolicy Bypass -File scripts/dr-drill.ps1",
"dr:health-check": "bash scripts/health-check.sh",
"dr:failover": "bash scripts/failover.sh"
},
"drDocs": {
"docs/dr/dr-plan.md": "灾备计划文档RTO/RPO 定义4h/24h、备份策略、故障切换流程、联系人列表、恢复步骤",
"docs/dr/dr-runbook.md": "灾备操作手册:数据库故障/应用故障/备份失败/异地同步失败/演练失败/磁盘不足场景的诊断与处理",
"docs/dr/reports/": "灾备演练报告存档目录Markdown 格式,由 dr-drill.sh 生成)",
"docs/dr/logs/": "故障切换日志目录(由 failover.sh 生成)"
},
"drEnvVars": {
"BACKUP_OFFSITE_BACKEND": "异地备份后端类型: s3|oss|nfs|none",
"BACKUP_OFFSITE_REMOTE": "远程存储路径",
"BACKUP_OFFSITE_BUCKET": "存储桶名称(仅 s3/oss",
"BACKUP_OFFSITE_ACCESS_KEY": "访问密钥",
"BACKUP_OFFSITE_SECRET_KEY": "秘密密钥",
"BACKUP_OFFSITE_REGION": "区域(默认 us-east-1",
"BACKUP_OFFSITE_RETENTION_DAYS": "远程备份保留天数(默认 90",
"DR_DRILL_TEST_DB": "演练测试数据库名(默认 next_edu_dr_drill",
"HEALTH_CHECK_URL": "应用健康检查 URL默认 http://localhost:8015",
"DATABASE_URL_STANDBY": "备库连接 URL故障切换时使用",
"FAILOVER_APP_NAME": "应用容器名(默认 nextjs-app"
},
"gitignore": {
"added": ["/backups/", "/audit-report.json", "/playwright-report/", "/test-results/"]
"added": ["/backups/", "/audit-report.json", "/trivy-fs-report.json", "/trivy-image-report.json", "/snyk.sarif", "/security-summary.md", "/playwright-report/", "/test-results/", "/tests/visual/.auth/"],
"exceptions": [".env.example (灾备环境变量示例,允许提交)"]
}
},
"testing": {
"e2e": {
"configFile": "playwright.config.ts",
"testDir": "./tests/e2e",
"testDir": "./tests",
"baseURL": "http://127.0.0.1:3000",
"webServer": {
"command": "npm run dev",
@@ -1891,7 +2084,15 @@
"DATABASE_URL": "mysql://test:test@127.0.0.1:3306/test_db"
}
},
"projects": [{"name": "chromium", "channel": "CI: undefined, local: chrome"}],
"projects": [
{"name": "chromium", "testDir": "./tests/e2e", "channel": "CI: undefined, local: chrome"},
{"name": "visual-chromium", "testDir": "./tests/visual", "channel": "CI: undefined, local: chrome"}
],
"snapshotPathTemplate": "{testDir}/__screenshots__/{testFilePath}/{arg}{ext}",
"expect": {
"toHaveScreenshot": {"maxDiffPixelRatio": 0.01, "animations": "disabled", "caret": "hide"},
"toMatchSnapshot": {"maxDiffPixelRatio": 0.01}
},
"retries": "CI: 2, local: 0",
"workers": "CI: 2, local: default",
"testFiles": {
@@ -1903,6 +2104,37 @@
"announcements.spec.ts": {"coverage": "公告页面未认证重定向 + 登录后渲染", "requiresDb": "partial"},
"grades.spec.ts": {"coverage": "成绩页面未认证重定向 + 登录后渲染", "requiresDb": "partial"}
}
},
"visual": {
"configFile": "tests/visual/visual.config.ts",
"snapshotDir": "tests/visual/__screenshots__",
"storageStateDir": "tests/visual/.auth/",
"viewports": {
"desktop": {"width": 1920, "height": 1080},
"tablet": {"width": 768, "height": 1024},
"mobile": {"width": 375, "height": 812}
},
"themes": ["light", "dark"],
"defaultMaxDiffPixelRatio": 0.01,
"testAccounts": {
"admin": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_ADMIN_EMAIL", "VISUAL_ADMIN_PASSWORD"]},
"teacher": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_TEACHER_EMAIL", "VISUAL_TEACHER_PASSWORD"]},
"student": {"email": "admin@xiaoxue.edu.cn", "envVars": ["VISUAL_STUDENT_EMAIL", "VISUAL_STUDENT_PASSWORD"]}
},
"testFiles": {
"homepage.spec.ts": {"coverage": "登录页在 desktop/tablet/mobile × light/dark 下的快照", "requiresDb": false},
"admin-dashboard.spec.ts": {"coverage": "管理员仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "admin"},
"teacher-dashboard.spec.ts": {"coverage": "教师仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "teacher"},
"student-dashboard.spec.ts": {"coverage": "学生仪表盘整页 + 侧边栏/主内容区组件快照", "requiresDb": true, "role": "student"}
},
"helpers": {
"auth.ts": ["setupAuthState(role)", "loginByUI(page, role)", "storageStatePath(role)"],
"visual-helpers.ts": ["setViewport(page, size)", "setTheme(page, theme)", "waitForPageReady(page)", "maskDynamicElements(page, selectors)", "buildMaskOption(masks)"]
},
"scripts": {
"test:visual": "playwright test --project=visual-chromium",
"test:visual:update": "playwright test --project=visual-chromium --update-snapshots"
}
}
}
}