feat(scripts): add diagnostic, seed, and test scripts
- Add add-ai-provider-visibility and add-missing-columns migration scripts - Add clear-error-book, seed-error-book, diagnose-error-book scripts - Add diagnose-tables and create-missing-tables scripts - Add test-failing-modules and test-teacher-pages test scripts
This commit is contained in:
395
scripts/test-teacher-pages.py
Normal file
395
scripts/test-teacher-pages.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
教师端全功能 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 = "<br>".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()
|
||||
Reference in New Issue
Block a user