""" 教师端全功能 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()