feat: add console visual QA tool

This commit is contained in:
Manus AI
2026-08-09 14:04:58 +08:00
parent fbea77f32d
commit 87f7095809
24 changed files with 1509 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
"""WP-CONSOLE-13 页面视觉、性能与失败证据工具测试。"""
from __future__ import annotations
import http.server
import json
import os
import socketserver
import subprocess
import sys
import threading
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "sdk/scripts/console_redesign_visual_qa.py"
class FixtureHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802 - stdlib handler contract
if self.path.startswith("/api/ok"):
payload = b'{"ok":true}'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
if self.path.startswith("/api/fail"):
payload = b'{"ok":false}'
self.send_response(503)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
return
if self.server.login_page:
body = """
<html><body><main><h1>登录总控平台</h1><p>请输入账号和密码。</p>
<form><label>账号<input name='username'></label><label>密码<input type='password' name='password'></label>
<button type='submit'>登录并加载平台</button></form></main></body></html>
"""
else:
body = """
<html><head><title>本地总控验收夹具</title></head><body>
<main id='business-page'><header><h1>数据总览</h1><p>工作手机总控平台业务页面,只读真实数据。</p></header>
<nav><a>手机设备</a><a>智能引擎</a><a>统一接入中心</a></nav>
<section><h2>数据已过期</h2><p>在线 待授权 离线;下载 APK扫码绑定单机控制台设备分组在线设备禁止删除。</p>
<p>真机写操作四证汇总;第三方应用与凭证列表;接口设备文档授权详情;第三方用量与额度看板;知识库文档。</p></section>
<script>fetch('/api/ok');</script></main></body></html>
"""
payload = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, *_args):
return
class FixtureServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
def __init__(self, server_address, handler, login_page=False):
self.login_page = login_page
super().__init__(server_address, handler)
def run_fixture(tmp_path: Path, *, login_page: bool = False, strict: bool = True) -> tuple[subprocess.CompletedProcess[str], Path]:
server = FixtureServer(("127.0.0.1", 0), FixtureHandler, login_page=login_page)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
output_dir = tmp_path / ("login-report" if login_page else "success-report")
try:
command = [sys.executable, str(SCRIPT), "--base-url", f"http://127.0.0.1:{server.server_address[1]}/", "--output-dir", str(output_dir)]
if strict:
command.append("--strict")
completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, env={**os.environ, "PYTHONUNBUFFERED": "1"}, timeout=90)
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
return completed, output_dir
def test_generates_18_screenshots_manifest_and_four_viewport_performance(tmp_path):
completed, output_dir = run_fixture(tmp_path)
assert completed.returncode == 0, completed.stderr + completed.stdout
report = json.loads((output_dir / "visual_qa_report.json").read_text(encoding="utf-8"))
manifest = json.loads((output_dir / "screenshot_manifest.json").read_text(encoding="utf-8"))
performance = json.loads((output_dir / "performance_report.json").read_text(encoding="utf-8"))
assert report["read_only"] is True
assert report["screenshot_spec_count"] == 18
assert report["screenshot_success_count"] == 18
assert report["interface_failure_count"] == 0
assert report["readonly_violation_count"] == 0
assert set(report["viewport_widths"]) == {1440, 1024, 768, 375}
assert set(performance["by_viewport"]) == {"1440", "1024", "768", "375"}
assert len(manifest["screenshots"]) == 18
for item in manifest["screenshots"]:
screenshot = Path(item["screenshot_path"])
assert screenshot.is_absolute()
assert screenshot.is_file()
assert item["id"] in screenshot.name
assert str(item["viewport"]["width"]) in screenshot.name
assert item["error_screenshot_path"] is None
assert len(list((output_dir / "screenshots").glob("*.png"))) == 18
def test_login_page_is_never_business_success_and_keeps_error_screenshots(tmp_path):
completed, output_dir = run_fixture(tmp_path, login_page=True)
assert completed.returncode != 0
report = json.loads((output_dir / "visual_qa_report.json").read_text(encoding="utf-8"))
manifest = json.loads((output_dir / "screenshot_manifest.json").read_text(encoding="utf-8"))
assert report["screenshot_success_count"] == 0
assert report["screenshot_failure_count"] == 18
assert report["business_page_rejection_count"] == 18
assert all(item["status"] == "failed" for item in manifest["screenshots"])
assert all("login_page" in item["reason"] for item in manifest["screenshots"])
assert all(Path(item["error_screenshot_path"]).is_file() for item in manifest["screenshots"])
def test_script_is_read_only_and_declares_all_required_screenshot_ids():
namespace: dict[str, object] = {}
namespace["__file__"] = str(SCRIPT)
exec(compile(SCRIPT.read_text(encoding="utf-8"), str(SCRIPT), "exec"), namespace)
specs = namespace["SCREENSHOTS"]
ids = {item.id for item in specs} # type: ignore[union-attr]
assert ids == {f"SS-{index:02d}" for index in range(1, 19)}
assert namespace["WRITE_METHODS"] == {"POST", "PUT", "PATCH", "DELETE"}