131 lines
6.2 KiB
Python
131 lines
6.2 KiB
Python
"""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"}
|