Files
workphone-sdk/sdk/tests/test_wp_console_13_visual_qa.py
2026-08-09 14:04:58 +08:00

131 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"}