feat: add console visual QA tool
This commit is contained in:
130
sdk/tests/test_wp_console_13_visual_qa.py
Normal file
130
sdk/tests/test_wp_console_13_visual_qa.py
Normal 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"}
|
||||
Reference in New Issue
Block a user