From cbc6e259fa0a57ac4990e777cadd4d5547d632db Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:23:55 +0800 Subject: [PATCH] chore(scripts): cleanup obsolete scripts and add new diagnostic scripts - Remove temp-i18n-zh-fix.mjs, test-failing-modules.py, test-teacher-pages.py - Add check-attendance-rules.mjs, check-db-state.mjs, test-modules.py --- scripts/check-attendance-rules.mjs | 37 +++ scripts/check-db-state.mjs | 35 +++ scripts/temp-i18n-zh-fix.mjs | 52 ---- scripts/test-failing-modules.py | 151 ----------- scripts/test-modules.py | 111 ++++++++ scripts/test-teacher-pages.py | 395 ----------------------------- 6 files changed, 183 insertions(+), 598 deletions(-) create mode 100644 scripts/check-attendance-rules.mjs create mode 100644 scripts/check-db-state.mjs delete mode 100644 scripts/temp-i18n-zh-fix.mjs delete mode 100644 scripts/test-failing-modules.py create mode 100644 scripts/test-modules.py delete mode 100644 scripts/test-teacher-pages.py diff --git a/scripts/check-attendance-rules.mjs b/scripts/check-attendance-rules.mjs new file mode 100644 index 0000000..b6b4a67 --- /dev/null +++ b/scripts/check-attendance-rules.mjs @@ -0,0 +1,37 @@ +/** + * 检查 attendance_rules 表的实际列结构。 + */ +import mysql from "mysql2/promise"; +import { config } from "dotenv"; + +config(); + +async function main() { + const conn = await mysql.createConnection(process.env.DATABASE_URL); + + console.log("=== attendance_rules 列结构 ==="); + const [cols] = await conn.query(`SHOW COLUMNS FROM \`attendance_rules\``); + console.table( + cols.map((c) => ({ + Field: c.Field, + Type: c.Type, + Null: c.Null, + Default: c.Default, + })) + ); + + console.log("\n=== leave_requests 表是否存在 ==="); + try { + const [rows] = await conn.query(`SHOW TABLES LIKE 'leave_requests'`); + console.log(rows.length > 0 ? "存在" : "不存在"); + } catch (err) { + console.log("查询失败:", err.message); + } + + await conn.end(); +} + +main().catch((err) => { + console.error("FATAL:", err); + process.exit(1); +}); diff --git a/scripts/check-db-state.mjs b/scripts/check-db-state.mjs new file mode 100644 index 0000000..1bea52b --- /dev/null +++ b/scripts/check-db-state.mjs @@ -0,0 +1,35 @@ +/** + * 检查重置后数据库的实际状态。 + */ +import mysql from "mysql2/promise"; +import { config } from "dotenv"; + +config(); + +async function main() { + const conn = await mysql.createConnection(process.env.DATABASE_URL); + + console.log("=== 数据库中的所有表 ==="); + const [tables] = await conn.query(`SHOW TABLES`); + if (tables.length === 0) { + console.log("(空数据库)"); + } else { + console.table(tables); + } + + // 如果 class_enrollments 存在,查看其结构 + try { + console.log("\n=== class_enrollments 结构 ==="); + const [cols] = await conn.query(`SHOW COLUMNS FROM \`class_enrollments\``); + console.table(cols); + } catch (err) { + console.log("class_enrollments 表不存在"); + } + + await conn.end(); +} + +main().catch((err) => { + console.error("FATAL:", err); + process.exit(1); +}); diff --git a/scripts/temp-i18n-zh-fix.mjs b/scripts/temp-i18n-zh-fix.mjs deleted file mode 100644 index b8704ae..0000000 --- a/scripts/temp-i18n-zh-fix.mjs +++ /dev/null @@ -1,52 +0,0 @@ -import fs from "node:fs"; -const f = "src/shared/i18n/messages/zh-CN/attendance.json"; -let c = fs.readFileSync(f, "utf8"); -const oldStr = `"noDataDescription": "当前班级在所选时间范围内暂无考勤记录。" - }, - "errors": {`; -const newStr = `"noDataDescription": "当前班级在所选时间范围内暂无考勤记录。" - }, - "correlation": { - "title": "考勤与成绩关联分析", - "description": "{className} · {start} 至 {end} · 出勤率与平均成绩的相关性", - "noData": "暂无关联分析数据", - "noDataDescription": "所选班级在时间范围内无考勤或成绩记录,无法进行关联分析。", - "noStudents": "暂无学生数据", - "noStudentsDescription": "该班级在时间范围内无考勤与成绩匹配的学生。", - "correlationCoefficient": "相关系数 r", - "notAvailable": "数据不足", - "scatterTitle": "出勤率-成绩散点图", - "scatterSeries": "学生", - "studentDetails": "学生明细", - "riskLevelLabel": "风险等级", - "attendanceRate": "出勤率", - "averageScore": "平均成绩", - "absentCount": "缺勤次数", - "gradeRecordCount": "成绩记录数", - "riskCounts": { - "high": "高风险学生数", - "medium": "中风险学生数", - "low": "低风险学生数" - }, - "riskLevel": { - "high": "高风险", - "medium": "中风险", - "low": "低风险" - }, - "interpretation": { - "strong_negative": "强负相关:出勤率越低,成绩越低", - "weak_negative": "弱负相关:出勤率与成绩有一定负相关", - "negligible": "无明显线性相关", - "weak_positive": "弱正相关:出勤率与成绩有一定正相关", - "strong_positive": "强正相关:出勤率越高,成绩越高", - "insufficient_data": "数据不足,无法计算相关性" - } - }, - "errors": {`; -if (!c.includes(oldStr)) { - console.log("OLD NOT FOUND"); - process.exit(1); -} -c = c.replace(oldStr, newStr); -fs.writeFileSync(f, c); -console.log("Done"); diff --git a/scripts/test-failing-modules.py b/scripts/test-failing-modules.py deleted file mode 100644 index 417344c..0000000 --- a/scripts/test-failing-modules.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -检查学情诊断、错题分析、公告、消息 4 个模块的实际渲染状态 -截取截图 + 捕获控制台错误 + 检查页面内容 -""" -import os -import time -from playwright.sync_api import sync_playwright - -BASE_URL = "http://localhost:3000" -TEACHER_EMAIL = "t_chinese_1@xiaoxue.edu.cn" -TEACHER_PASSWORD = "123456" -SCREENSHOT_DIR = os.path.join(os.path.dirname(__file__), "..", "bugs", "screenshots") -os.makedirs(SCREENSHOT_DIR, exist_ok=True) - -ROUTES = [ - ("/teacher/diagnostic", "学情诊断"), - ("/teacher/error-book", "错题分析"), - ("/announcements", "公告"), - ("/messages", "消息"), -] - - -def main(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - context = browser.new_context(viewport={"width": 1440, "height": 900}) - page = context.new_page() - - # Login - print(">>> 登录...") - page.goto(f"{BASE_URL}/login", wait_until="domcontentloaded") - page.wait_for_timeout(3000) - page.locator('input[name="email"]').fill(TEACHER_EMAIL) - page.locator('input[type="password"]').first.fill(TEACHER_PASSWORD) - page.evaluate("""() => { - const form = document.querySelector('form'); - if (form) { - const event = new Event('submit', { cancelable: true, bubbles: true }); - form.dispatchEvent(event); - } - }""") - page.wait_for_timeout(5000) - print(f"登录后 URL: {page.url}") - - for route, name in ROUTES: - print(f"\n{'='*60}") - print(f">>> 测试: {name} ({route})") - print(f"{'='*60}") - - console_errors = [] - console_warnings = [] - - def on_console(msg): - if msg.type == "error": - console_errors.append(msg.text) - elif msg.type == "warning": - console_warnings.append(msg.text) - - def on_page_error(err): - console_errors.append(f"PageError: {str(err)[:300]}") - - page.on("console", on_console) - page.on("pageerror", on_page_error) - - try: - response = page.goto(f"{BASE_URL}{route}", timeout=30000, wait_until="domcontentloaded") - page.wait_for_timeout(3000) # Wait for client-side JS to execute - - print(f" HTTP Status: {response.status if response else 'N/A'}") - print(f" Final URL: {page.url}") - - # Check for error indicators on page - body_text = page.locator("body").text_content() or "" - print(f" Body text length: {len(body_text)}") - - # Check for common error patterns - error_elements = page.locator('[role="alert"], .text-destructive, .text-red-500, .text-red-600') - error_count = error_elements.count() - if error_count > 0: - print(f" Error elements on page: {error_count}") - for i in range(min(error_count, 5)): - try: - text = error_elements.nth(i).text_content() - if text and text.strip(): - print(f" [{i}] {text.strip()[:200]}") - except: - pass - - # Check for loading spinners still visible - spinners = page.locator('.animate-spin, [role="status"], .loading') - spinner_count = spinners.count() - if spinner_count > 0: - print(f" Loading spinners still visible: {spinner_count}") - - # Check for empty state - if "暂无" in body_text or "No data" in body_text or "没有" in body_text: - # Find the context - for keyword in ["暂无", "No data", "没有"]: - idx = body_text.find(keyword) - if idx >= 0: - context_text = body_text[max(0, idx-30):idx+80] - print(f" Empty state: ...{context_text}...") - break - - # Check for "加载失败" or "error" text - for keyword in ["加载失败", "load", "error", "错误", "失败", "Error"]: - if keyword.lower() in body_text.lower(): - idx = body_text.lower().find(keyword.lower()) - context_text = body_text[max(0, idx-30):idx+100] - print(f" Found '{keyword}': ...{context_text}...") - break - - # Take screenshot - screenshot_name = route.replace("/", "_").strip("_") + ".png" - screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name) - page.screenshot(path=screenshot_path, full_page=True) - print(f" Screenshot: {screenshot_path}") - - # Print console errors - if console_errors: - print(f"\n Console Errors ({len(console_errors)}):") - for err in console_errors[:10]: - print(f" - {err[:300]}") - else: - print(f" Console Errors: 0") - - if console_warnings: - print(f"\n Console Warnings ({len(console_warnings)}):") - for w in console_warnings[:5]: - print(f" - {w[:200]}") - - except Exception as e: - print(f" EXCEPTION: {type(e).__name__}: {str(e)[:300]}") - screenshot_name = route.replace("/", "_").strip("_") + "_error.png" - screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name) - try: - page.screenshot(path=screenshot_path, full_page=True) - except: - pass - finally: - page.remove_listener("console", on_console) - page.remove_listener("pageerror", on_page_error) - - browser.close() - print(f"\n{'='*60}") - print("测试完成!") - print(f"{'='*60}") - - -if __name__ == "__main__": - main() diff --git a/scripts/test-modules.py b/scripts/test-modules.py new file mode 100644 index 0000000..ef6d832 --- /dev/null +++ b/scripts/test-modules.py @@ -0,0 +1,111 @@ +""" +验证 6 个模块的页面是否能正常加载。 +检测方式: + 1. HTTP 状态码(非 200 即失败) + 2. JavaScript pageerror + 3. 可见错误边界元素([data-error-boundary], [role="alert"] 中的错误文本) + 注意:不检查 content 中的 i18n 文本(翻译 JSON 会被误判) +""" +import sys +from playwright.sync_api import sync_playwright + +BASE = "http://localhost:3000" +EMAIL = "t_chinese_1@xiaoxue.edu.cn" +PASSWORD = "123456" + +MODULES = [ + ("考勤统计", "/teacher/attendance/stats"), + ("学情诊断", "/teacher/diagnostic"), + ("错题分析", "/teacher/error-book"), + ("选修课", "/teacher/elective"), + ("公告", "/announcements"), + ("消息", "/messages"), +] + + +def login(page): + page.goto(f"{BASE}/login", wait_until="domcontentloaded", timeout=15000) + page.wait_for_timeout(1000) + + email_input = page.locator('input[type="email"]') + if email_input.count() == 0: + email_input = page.locator('input[name="email"]') + email_input.fill(EMAIL) + + pwd_input = page.locator('input[type="password"]') + pwd_input.fill(PASSWORD) + + page.get_by_role("button", name="登录").click() + page.wait_for_url("**/dashboard**", timeout=15000) + page.wait_for_timeout(2000) + + +def main(): + results = [] + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + + page = browser.new_page() + all_errors = [] + page.on("pageerror", lambda err: all_errors.append(str(err)[:300])) + + print("登录中...") + login(page) + print(f"登录成功,URL: {page.url}") + + for name, path in MODULES: + url = f"{BASE}{path}" + all_errors.clear() + + try: + response = page.goto(url, wait_until="domcontentloaded", timeout=30000) + page.wait_for_timeout(4000) + + http_status = response.status if response else 0 + + # 检查可见错误边界(排除 i18n JSON 文本) + # 只查找实际渲染的错误提示元素 + visible_error = "" + error_locators = [ + 'text="发生未知错误"', + 'text="Something went wrong"', + '[data-error-boundary]', + ] + for sel in error_locators: + loc = page.locator(sel).first + if loc.count() > 0 and loc.is_visible(): + visible_error = sel + break + + status = "FAIL" + error_info = "" + if http_status != 200: + status = "FAIL" + error_info = f"HTTP {http_status}" + elif len(all_errors) > 0: + status = "FAIL" + error_info = "PageError: " + " | ".join(all_errors[:2]) + elif visible_error: + status = "FAIL" + error_info = f"可见错误: {visible_error}" + else: + status = "PASS" + + results.append((name, status, error_info)) + print(f"[{status}] {name}: {error_info or 'OK'} (HTTP {http_status})") + except Exception as e: + results.append((name, "FAIL", str(e)[:100])) + print(f"[FAIL] {name}: {str(e)[:100]}") + + browser.close() + + print("\n=== 汇总 ===") + pass_count = sum(1 for _, s, _ in results if s == "PASS") + fail_count = sum(1 for _, s, _ in results if s == "FAIL") + print(f"总计: {pass_count} PASS, {fail_count} FAIL") + + sys.exit(0 if fail_count == 0 else 1) + + +if __name__ == "__main__": + main() diff --git a/scripts/test-teacher-pages.py b/scripts/test-teacher-pages.py deleted file mode 100644 index 403c1dd..0000000 --- a/scripts/test-teacher-pages.py +++ /dev/null @@ -1,395 +0,0 @@ -""" -教师端全功能 Web 测试 (Post-Audit) -测试范围: 所有教师端页面路由 + 详情页发现 + 控制台错误捕获 -""" -import json -import os -import time -from playwright.sync_api import sync_playwright - -BASE_URL = "http://localhost:3000" -TEACHER_EMAIL = "t_chinese_1@xiaoxue.edu.cn" -TEACHER_PASSWORD = "123456" -SCREENSHOT_DIR = os.path.join(os.path.dirname(__file__), "..", "bugs", "screenshots") -os.makedirs(SCREENSHOT_DIR, exist_ok=True) - -# 教师端所有路由 -TEACHER_ROUTES = [ - {"category": "Dashboard", "routes": ["/teacher/dashboard"]}, - {"category": "Textbooks", "routes": ["/teacher/textbooks"]}, - {"category": "Questions", "routes": ["/teacher/questions"]}, - {"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": "Classes", "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": "Elective", "routes": ["/teacher/elective"]}, - {"category": "Error Book", "routes": ["/teacher/error-book"]}, -] - -# 详情页发现模式 -DETAIL_PATTERNS = [ - {"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/"}, - {"category": "Homework Detail", "listRoute": "/teacher/homework/assignments", "linkPattern": "/teacher/homework/assignments/"}, - {"category": "Exams Detail", "listRoute": "/teacher/exams/all", "linkPattern": "/teacher/exams/"}, -] - - -class TestResult: - def __init__(self, url, category): - self.url = url - self.category = category - self.status = "unknown" - self.http_status = None - self.final_url = url - self.errors = [] - self.warnings = [] - self.screenshot = None - - -all_results = [] - - -def add_result(result): - all_results.append(result) - - -def test_single_page(page, route, category): - result = TestResult(route, category) - console_errors = [] - - def on_console(msg): - if msg.type == "error": - console_errors.append(msg.text) - - page.on("console", on_console) - - # Also capture page errors (uncaught exceptions) - def on_page_error(err): - console_errors.append(f"PageError: {str(err)[:200]}") - - page.on("pageerror", on_page_error) - - try: - response = page.goto(f"{BASE_URL}{route}", timeout=30000, wait_until="domcontentloaded") - page.wait_for_timeout(2000) # Wait for JS to execute and render - - result.http_status = response.status if response else None - result.final_url = page.url - - http_status = result.http_status - final_url = result.final_url - - if http_status and http_status >= 500: - result.status = "failed" - result.errors.append(f"HTTP {http_status} error") - elif http_status and http_status >= 400: - result.status = "warning" - result.warnings.append(f"HTTP {http_status} error") - elif "/login" in final_url: - result.status = "failed" - result.errors.append("Redirected to login - auth issue") - elif final_url.rstrip("/").endswith("/error") or "/500" in final_url: - result.status = "failed" - result.errors.append("Redirected to error page") - else: - result.status = "passed" - - # Check for error elements on page - error_elements = page.locator('[role="alert"], .text-destructive, .text-red-500') - error_count = error_elements.count() - if error_count > 0: - for i in range(min(error_count, 3)): - try: - text = error_elements.nth(i).text_content() - if text and text.strip(): - result.warnings.append(f"Error text: {text.strip()[:100]}") - except: - pass - - # Check if page is empty - body_text = page.locator("body").text_content() or "" - if len(body_text.strip()) < 50: - result.warnings.append("Page appears empty") - - if console_errors: - result.errors.extend(console_errors[:5]) - - # Take screenshot for failed/warning pages - if result.status in ("failed", "warning"): - screenshot_name = route.replace("/", "_").strip("_") + ".png" - screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name) - try: - page.screenshot(path=screenshot_path, full_page=True) - result.screenshot = screenshot_path - except: - pass - - icon = "PASS" if result.status == "passed" else ("WARN" if result.status == "warning" else "FAIL") - print(f" [{icon}] {result.status} (HTTP {result.http_status}) - {route}") - - except Exception as e: - result.status = "failed" - err_msg = str(e)[:200] - result.errors.append(f"Exception: {err_msg}") - print(f" [FAIL] ERROR: {err_msg[:100]} - {route}") - - # Try screenshot even on failure - try: - screenshot_name = route.replace("/", "_").strip("_") + "_error.png" - screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name) - page.screenshot(path=screenshot_path, full_page=True) - result.screenshot = screenshot_path - except: - pass - finally: - page.remove_listener("console", on_console) - page.remove_listener("pageerror", on_page_error) - - return result - - -def discover_detail_links(page, list_route, link_pattern): - urls = [] - try: - page.goto(f"{BASE_URL}{list_route}", timeout=25000, wait_until="domcontentloaded") - page.wait_for_timeout(2000) - - links = page.locator(f'a[href*="{link_pattern}"]') - count = links.count() - seen = set() - for i in range(min(count, 10)): - href = links.nth(i).get_attribute("href") - if href and link_pattern in href and href not in seen: - # Avoid the list page itself - if href != list_route: - seen.add(href) - urls.append(href) - except Exception as e: - print(f" Warning: Failed to discover links on {list_route}: {e}") - - return urls[:3] # Max 3 detail pages per category - - -def generate_report(): - lines = [] - passed = sum(1 for r in all_results if r.status == "passed") - failed = sum(1 for r in all_results if r.status == "failed") - warnings = sum(1 for r in all_results if r.status == "warning") - total = len(all_results) - - lines.append("# 教师端 Web 功能测试报告 (Post-Audit)") - lines.append("") - lines.append(f"> 测试日期: {time.strftime('%Y-%m-%d %H:%M:%S')}") - lines.append("> 测试范围: 所有教师端页面功能 (审计修复后)") - lines.append("> 测试工具: Playwright + Chromium (Python)") - lines.append(f"> 测试账号: {TEACHER_EMAIL}") - lines.append("") - lines.append("---") - lines.append("") - lines.append("## 一、测试概览") - lines.append("") - lines.append("| 指标 | 数值 |") - lines.append("|------|------|") - lines.append(f"| 总测试页面数 | {total} |") - lines.append(f"| PASS | {passed} |") - lines.append(f"| FAIL | {failed} |") - lines.append(f"| WARN | {warnings} |") - pass_rate = f"{(passed / total * 100):.1f}%" if total > 0 else "N/A" - lines.append(f"| 通过率 | {pass_rate} |") - lines.append("") - lines.append("---") - lines.append("") - lines.append("## 二、页面测试详情") - lines.append("") - - # Group by category - by_category = {} - for r in all_results: - if r.category not in by_category: - by_category[r.category] = [] - by_category[r.category].append(r) - - for category, results in by_category.items(): - lines.append(f"### {category}") - lines.append("") - lines.append("| 页面 | HTTP状态 | 结果 | 备注 |") - lines.append("|------|----------|------|------|") - - for r in results: - icon = "PASS" if r.status == "passed" else ("WARN" if r.status == "warning" else "FAIL") - notes = [] - if r.final_url != f"{BASE_URL}{r.url}" and r.final_url != r.url: - notes.append(f"重定向: {r.final_url}") - if r.errors: - notes.append(f"错误: {'; '.join(r.errors[:2])}") - if r.warnings: - notes.append(f"警告: {'; '.join(r.warnings[:2])}") - note_str = "
".join(notes) if notes else "-" - - lines.append(f"| {icon} `{r.url}` | {r.http_status or '-'} | {r.status} | {note_str} |") - - lines.append("") - - # Failed details - failed_results = [r for r in all_results if r.status == "failed"] - if failed_results: - lines.append("---") - lines.append("") - lines.append("## 三、失败页面详情") - lines.append("") - for r in failed_results: - lines.append(f"### FAIL `{r.url}`") - lines.append("") - lines.append(f"- **分类**: {r.category}") - lines.append(f"- **HTTP状态**: {r.http_status or '-'}") - if r.final_url != r.url: - lines.append(f"- **重定向**: {r.final_url}") - if r.errors: - lines.append("- **错误信息**:") - for e in r.errors: - lines.append(f" - {e}") - if r.screenshot: - lines.append(f"- **截图**: {r.screenshot}") - lines.append("") - - # Warning details - warning_results = [r for r in all_results if r.status == "warning"] - if warning_results: - lines.append("---") - lines.append("") - lines.append("## 四、警告页面") - lines.append("") - for r in warning_results: - lines.append(f"### WARN `{r.url}`") - lines.append("") - lines.append(f"- **分类**: {r.category}") - if r.warnings: - for w in r.warnings: - lines.append(f" - {w}") - if r.screenshot: - lines.append(f"- **截图**: {r.screenshot}") - lines.append("") - - lines.append("---") - lines.append("") - lines.append(f"*报告自动生成于 {time.strftime('%Y-%m-%d %H:%M:%S')}*") - - return "\n".join(lines) - - -def main(): - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - context = browser.new_context(viewport={"width": 1440, "height": 900}) - page = context.new_page() - - # ====== Step 1: Login ====== - print("\n>>> 登录教师账号...") - page.goto(f"{BASE_URL}/login", wait_until="domcontentloaded") - page.wait_for_timeout(3000) # Wait for form to fully render - - # Fill login form - email_input = page.locator('input[name="email"]') - if email_input.count() == 0: - email_input = page.locator('input[type="email"]').first - - email_input.fill(TEACHER_EMAIL) - page.locator('input[type="password"], input[name="password"]').first.fill(TEACHER_PASSWORD) - - # Submit form via JavaScript (avoids Next.js dev overlay intercepting clicks) - page.evaluate("""() => { - const form = document.querySelector('form'); - if (form) { - const event = new Event('submit', { cancelable: true, bubbles: true }); - form.dispatchEvent(event); - } - }""") - - page.wait_for_timeout(5000) # Wait for redirect after login - - current_url = page.url - print(f"登录后 URL: {current_url}") - - if "/login" in current_url: - print("FAIL: 登录失败,仍在登录页!") - # Take screenshot of login failure - page.screenshot(path=os.path.join(SCREENSHOT_DIR, "login_failure.png"), full_page=True) - browser.close() - return - - print("PASS: 登录成功!") - - # ====== Step 2: Test all routes ====== - print("\n>>> 测试所有教师端路由...") - for group in TEACHER_ROUTES: - category = group["category"] - routes = group["routes"] - print(f"\n === {category} ===") - for route in routes: - print(f" 测试: {route}") - result = test_single_page(page, route, category) - add_result(result) - - # ====== Step 3: Discover and test detail pages ====== - print("\n\n>>> 发现详情页链接...") - for pattern in DETAIL_PATTERNS: - category = pattern["category"] - list_route = pattern["listRoute"] - link_pattern = pattern["linkPattern"] - print(f"\n 发现: {category} (from {list_route})") - detail_urls = discover_detail_links(page, list_route, link_pattern) - if detail_urls: - print(f" 找到 {len(detail_urls)} 个详情页") - for detail_url in detail_urls: - result = test_single_page(page, detail_url, category) - add_result(result) - else: - print(f" 未发现详情页链接") - - # ====== Step 4: Generate report ====== - report = generate_report() - bugs_dir = os.path.join(os.path.dirname(__file__), "..", "bugs") - os.makedirs(bugs_dir, exist_ok=True) - output_path = os.path.join(bugs_dir, "teacher_web_test_post_audit.md") - with open(output_path, "w", encoding="utf-8") as f: - f.write(report) - - # Also save JSON results - json_path = os.path.join(bugs_dir, "teacher_web_test_post_audit.json") - json_data = [] - for r in all_results: - json_data.append({ - "url": r.url, - "category": r.category, - "status": r.status, - "http_status": r.http_status, - "final_url": r.final_url, - "errors": r.errors, - "warnings": r.warnings, - }) - with open(json_path, "w", encoding="utf-8") as f: - json.dump(json_data, f, ensure_ascii=False, indent=2) - - passed = sum(1 for r in all_results if r.status == "passed") - failed = sum(1 for r in all_results if r.status == "failed") - warnings = sum(1 for r in all_results if r.status == "warning") - - print(f"\n{'=' * 60}") - print(f"测试完成: 总计 {len(all_results)}, PASS {passed}, FAIL {failed}, WARN {warnings}") - print(f"报告已写入: {output_path}") - print(f"JSON 结果: {json_path}") - print(f"{'=' * 60}") - - browser.close() - - -if __name__ == "__main__": - main()