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
This commit is contained in:
SpecialX
2026-07-04 10:23:55 +08:00
parent 4b6cb5f11e
commit cbc6e259fa
6 changed files with 183 additions and 598 deletions

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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");

View File

@@ -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()

111
scripts/test-modules.py Normal file
View File

@@ -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()

View File

@@ -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 = "<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()