/** * 教师端全功能 Web 测试 * 测试范围:教师端所有页面路由 */ import { expect, test } from "@playwright/test" import fs from "node:fs" import path from "node:path" const TEACHER_EMAIL = "t_chinese_1@xiaoxue.edu.cn" const TEACHER_PASSWORD = "123456" // 教师端所有路由 const TEACHER_ROUTES: { category: string; routes: string[] }[] = [ { category: "Dashboard", routes: ["/teacher/dashboard"], }, { category: "Textbooks", routes: ["/teacher/textbooks"], }, { category: "Exams", routes: [ "/teacher/exams", "/teacher/exams/all", "/teacher/exams/create", ], }, { category: "Homework", routes: [ "/teacher/homework", "/teacher/homework/assignments", "/teacher/homework/assignments/create", "/teacher/homework/submissions", ], }, { category: "Grades", routes: [ "/teacher/grades", "/teacher/grades/entry", "/teacher/grades/stats", "/teacher/grades/analytics", ], }, { category: "Question Bank", routes: ["/teacher/questions"], }, { category: "Class Management", routes: [ "/teacher/classes", "/teacher/classes/my", "/teacher/classes/students", "/teacher/classes/schedule", ], }, { category: "Course Plans", routes: ["/teacher/course-plans"], }, { category: "Lesson Plans", routes: [ "/teacher/lesson-plans", "/teacher/lesson-plans/new", ], }, { category: "Attendance", routes: [ "/teacher/attendance", "/teacher/attendance/sheet", "/teacher/attendance/stats", ], }, { category: "Schedule Changes", routes: ["/teacher/schedule-changes"], }, { category: "Diagnostic", routes: ["/teacher/diagnostic"], }, { category: "Electives", routes: ["/teacher/elective"], }, { category: "Management", routes: [ "/management/grade/classes", "/management/grade/insights", ], }, { category: "Announcements", routes: ["/announcements"], }, { category: "Messages", routes: ["/messages"], }, ] // 需要登录后从列表页抓取链接的详情页模式 const DETAIL_PATTERNS: { category: string; listRoute: string; linkPattern: string }[] = [ { category: "Textbooks Detail", listRoute: "/teacher/textbooks", linkPattern: "/teacher/textbooks/" }, { category: "Classes Detail", listRoute: "/teacher/classes/my", linkPattern: "/teacher/classes/my/" }, { category: "Course Plans Detail", listRoute: "/teacher/course-plans", linkPattern: "/teacher/course-plans/" }, { category: "Lesson Plans Detail", listRoute: "/teacher/lesson-plans", linkPattern: "/teacher/lesson-plans/" }, ] interface TestResult { url: string category: string status: "passed" | "failed" | "warning" httpStatus: number | null finalUrl: string errors: string[] warnings: string[] } const allResults: TestResult[] = [] function addResult(result: TestResult): void { allResults.push(result) } function generateReport(): string { const lines: string[] = [] const passed = allResults.filter((r) => r.status === "passed").length const failed = allResults.filter((r) => r.status === "failed").length const warnings = allResults.filter((r) => r.status === "warning").length const total = allResults.length lines.push("# 教师端 Web 功能测试报告 (v2)") lines.push("") lines.push(`> 测试日期:${new Date().toISOString().replace("T", " ").slice(0, 19)}`) lines.push("> 测试范围:所有教师端页面功能") lines.push("> 测试工具:Playwright + Chromium") lines.push(`> 测试账号:${TEACHER_EMAIL}`) lines.push("") lines.push("---") lines.push("") lines.push("## 一、测试概览") lines.push("") lines.push("| 指标 | 数值 |") lines.push("|------|------|") lines.push(`| 总测试页面数 | ${total} |`) lines.push(`| ✅ 通过 | ${passed} |`) lines.push(`| ❌ 失败 | ${failed} |`) lines.push(`| ⚠️ 警告 | ${warnings} |`) lines.push(`| 通过率 | ${total > 0 ? ((passed / total) * 100).toFixed(1) : "N/A"}% |`) lines.push("") lines.push("---") lines.push("") lines.push("## 二、页面测试详情") lines.push("") // 按类别分组 const byCategory = new Map() for (const r of allResults) { const cat = r.category if (!byCategory.has(cat)) byCategory.set(cat, []) byCategory.get(cat)!.push(r) } for (const [category, results] of byCategory) { lines.push(`### ${category}`) lines.push("") lines.push("| 页面 | HTTP状态 | 结果 | 备注 |") lines.push("|------|----------|------|------|") for (const r of results) { const icon = r.status === "passed" ? "✅" : r.status === "warning" ? "⚠️" : "❌" const notes: string[] = [] if (r.finalUrl !== r.url) notes.push(`重定向: ${r.finalUrl}`) if (r.errors.length > 0) notes.push(`错误: ${r.errors.slice(0, 2).join("; ")}`) if (r.warnings.length > 0) notes.push(`警告: ${r.warnings.slice(0, 2).join("; ")}`) const noteStr = notes.length > 0 ? notes.join("
") : "-" lines.push(`| ${icon} \`${r.url}\` | ${r.httpStatus ?? "-"} | ${r.status} | ${noteStr} |`) } lines.push("") } // 失败详情 const failedResults = allResults.filter((r) => r.status === "failed") if (failedResults.length > 0) { lines.push("---") lines.push("") lines.push("## 三、失败页面详情") lines.push("") for (const r of failedResults) { lines.push(`### ❌ \`${r.url}\``) lines.push("") lines.push(`- **分类**: ${r.category}`) lines.push(`- **HTTP状态**: ${r.httpStatus ?? "-"}`) if (r.finalUrl !== r.url) lines.push(`- **重定向**: ${r.finalUrl}`) if (r.errors.length > 0) { lines.push("- **错误信息**:") for (const e of r.errors) lines.push(` - ${e}`) } lines.push("") } } // 警告详情 const warningResults = allResults.filter((r) => r.status === "warning") if (warningResults.length > 0) { lines.push("---") lines.push("") lines.push("## 四、警告页面") lines.push("") for (const r of warningResults) { lines.push(`### ⚠️ \`${r.url}\``) lines.push("") lines.push(`- **分类**: ${r.category}`) if (r.warnings.length > 0) { for (const w of r.warnings) lines.push(` - ${w}`) } lines.push("") } } lines.push("---") lines.push("") lines.push(`*报告自动生成于 ${new Date().toISOString().replace("T", " ").slice(0, 19)}*`) return lines.join("\n") } test.describe("Teacher Full Web Test", () => { test.setTimeout(300000) // 5 minutes total test("login and test all teacher pages", async ({ page }) => { // ====== Step 1: Login ====== console.log("\n>>> 登录教师账号...") await page.goto("/login") await page.waitForLoadState("networkidle") // 填表登录 const emailInput = page.locator('input[name="email"]') if ((await emailInput.count()) === 0) { await page.locator('input[type="email"]').first().waitFor({ state: "visible", timeout: 5000 }) } await page.locator('input[type="email"], input[name="email"]').first().fill(TEACHER_EMAIL) await page.locator('input[type="password"], input[name="password"]').first().fill(TEACHER_PASSWORD) // 点击登录 const submitBtn = page.locator('button[type="submit"]') if ((await submitBtn.count()) > 0) { await submitBtn.first().click() } else { await page.getByRole("button", { name: /Sign In|登录/i }).click() } await page.waitForLoadState("networkidle") await page.waitForTimeout(2000) console.log(`登录后 URL: ${page.url()}`) expect(page.url()).not.toContain("/login") // ====== Step 2: Test all routes ====== for (const { category, routes } of TEACHER_ROUTES) { for (const route of routes) { console.log(`\n 测试: ${category} - ${route}`) const result = await testSinglePage(page, route, category) addResult(result) } } // ====== Step 3: Discover and test detail pages ====== console.log("\n\n>>> 发现详情页链接...") for (const { category, listRoute, linkPattern } of DETAIL_PATTERNS) { const detailUrls = await discoverDetailLinks(page, listRoute, linkPattern) if (detailUrls.length > 0) { console.log(`\n ${category}: ${detailUrls.length} 个详情页`) for (const detailUrl of detailUrls.slice(0, 2)) { const result = await testSinglePage(page, detailUrl, `${category}`) addResult(result) } } else { console.log(`\n ${category}: 未发现详情页链接`) } } // ====== Step 4: Generate report ====== const report = generateReport() const bugsDir = path.resolve(__dirname, "..", "..", "bugs") fs.mkdirSync(bugsDir, { recursive: true }) const outputPath = path.join(bugsDir, "教师端_web_test2.md") fs.writeFileSync(outputPath, report, "utf-8") console.log(`\n\n${"=".repeat(60)}`) const passed = allResults.filter((r) => r.status === "passed").length const failed = allResults.filter((r) => r.status === "failed").length console.log(`测试完成: 总计 ${allResults.length}, 通过 ${passed}, 失败 ${failed}`) console.log(`报告已写入: ${outputPath}`) console.log(`${"=".repeat(60)}`) }) }) async function testSinglePage( page: import("@playwright/test").Page, route: string, category: string, ): Promise { const result: TestResult = { url: route, category, status: "unknown", httpStatus: null, finalUrl: route, errors: [], warnings: [], } const consoleErrors: string[] = [] const onConsole = (msg: import("@playwright/test").ConsoleMessage) => { if (msg.type() === "error") { consoleErrors.push(msg.text()) } } page.on("console", onConsole) try { const response = await page.goto(route, { timeout: 25000 }) await page.waitForLoadState("networkidle", { timeout: 15000 }) await page.waitForTimeout(500) result.httpStatus = response?.status() ?? null result.finalUrl = page.url() const httpStatus = result.httpStatus const finalUrl = result.finalUrl if (httpStatus && httpStatus >= 500) { result.status = "failed" result.errors.push(`HTTP ${httpStatus} error`) } else if (httpStatus && httpStatus >= 400) { result.status = "warning" result.warnings.push(`HTTP ${httpStatus} error`) } else if (finalUrl.includes("/login")) { result.status = "failed" result.errors.push("Redirected to login - auth issue") } else if (finalUrl.includes("/error") || finalUrl.includes("/500")) { result.status = "failed" result.errors.push("Redirected to error page") } else { result.status = "passed" } // 检查页面错误提示 const errorElements = page.locator('[role="alert"], .text-destructive, .text-red-500') const errorCount = await errorElements.count() if (errorCount > 0) { for (let i = 0; i < Math.min(errorCount, 3); i++) { const text = await errorElements.nth(i).textContent() if (text && text.trim()) { result.warnings.push(`Error text: ${text.trim().slice(0, 100)}`) } } } // 检查页面是否为空 const bodyText = (await page.locator("body").textContent()) ?? "" if (bodyText.trim().length < 50) { result.warnings.push("Page appears empty") } if (consoleErrors.length > 0) { result.errors.push(...consoleErrors.slice(0, 5)) } const icon = result.status === "passed" ? "✅" : result.status === "warning" ? "⚠️" : "❌" console.log(` ${icon} ${result.status} (HTTP ${result.httpStatus})`) } catch (e) { result.status = "failed" result.errors.push(`Exception: ${String(e).slice(0, 200)}`) console.log(` ❌ ERROR: ${String(e).slice(0, 100)}`) } finally { page.removeListener("console", onConsole) } return result } async function discoverDetailLinks( page: import("@playwright/test").Page, listRoute: string, linkPattern: string, ): Promise { try { await page.goto(listRoute, { timeout: 25000 }) await page.waitForLoadState("networkidle", { timeout: 15000 }) await page.waitForTimeout(500) const links = page.locator(`a[href*="${linkPattern}"]`) const count = await links.count() const detailUrls: string[] = [] for (let i = 0; i < Math.min(count, 3); i++) { const href = await links.nth(i).getAttribute("href") if (href && href !== listRoute && href.includes(linkPattern)) { detailUrls.push(href) } } return detailUrls } catch { return [] } }