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,514 @@
#!/usr/bin/env python3
"""总控平台页面视觉与性能验收工具。
默认只读:只访问 BASE_URL 的 GET 页面和接口,不点击业务按钮、不调用手机、不执行写接口。
如需登录,使用 CONSOLE_QA_USERNAME/CONSOLE_QA_PASSWORD或 USERNAME/PASSWORD环境变量。
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import re
import shutil
import sys
import time
from collections import Counter
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
try:
from PIL import Image, ImageStat
except ImportError as exc: # pragma: no cover - 运行环境缺依赖时给出明确提示
Image = None # type: ignore[assignment]
ImageStat = None # type: ignore[assignment]
_PIL_IMPORT_ERROR = exc
else:
_PIL_IMPORT_ERROR = None
try:
from playwright.async_api import Browser, BrowserContext, Page, async_playwright
except ImportError as exc: # pragma: no cover - 运行环境缺依赖时给出明确提示
Browser = BrowserContext = Page = Any # type: ignore[misc,assignment]
async_playwright = None # type: ignore[assignment]
_PLAYWRIGHT_IMPORT_ERROR = exc
else:
_PLAYWRIGHT_IMPORT_ERROR = None
ROOT = Path(__file__).resolve().parents[2]
DEFAULT_OUTPUT_DIR = ROOT / "开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA"
VIEWPORTS = {
1440: 1200,
1024: 1100,
768: 1024,
375: 900,
}
WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
AUTH_MARKERS = ("登录总控平台", "登录并加载平台", "账号", "密码", "退出登录")
BUSINESS_MARKERS = ("数据总览", "手机设备", "智能引擎", "统一接入中心")
@dataclass(frozen=True)
class ScreenshotSpec:
id: str
title: str
tab: str
width: int
query: tuple[tuple[str, str], ...] = ()
required_tokens: tuple[str, ...] = ()
SCREENSHOTS = (
ScreenshotSpec("SS-01", "数据总览 1440px", "overview", 1440, required_tokens=("数据总览",)),
ScreenshotSpec("SS-02", "数据总览接口异常/数据过期", "overview", 1440, (("qa_state", "expired"),), ("数据总览", "数据已过期")),
ScreenshotSpec("SS-03", "手机设备含下载与扫码按钮", "devices", 1024, required_tokens=("手机设备",)),
ScreenshotSpec("SS-04", "添加设备 APK 下载步骤", "devices", 1024, (("qa_step", "apk"),), ("手机设备",)),
ScreenshotSpec("SS-05", "添加设备原扫码绑定步骤", "devices", 1024, (("qa_step", "scan"),), ("手机设备",)),
ScreenshotSpec("SS-06", "设备卡片三种状态", "devices", 1024, (("qa_state", "statuses"),), ("手机设备",)),
ScreenshotSpec("SS-07", "单机控制台概览", "devices", 1024, (("view", "device"),), ("手机设备",)),
ScreenshotSpec("SS-08", "设备分组管理", "devices", 1024, (("view", "groups"),), ("手机设备",)),
ScreenshotSpec("SS-09", "在线设备禁止删除", "devices", 1024, (("qa_state", "delete-protected"),), ("手机设备",)),
ScreenshotSpec("SS-10", "智能引擎四模块", "engine", 1024, required_tokens=("智能引擎",)),
ScreenshotSpec("SS-11", "统一接入中心接口目录", "integrations", 1024, (("view", "api"),), ("统一接入中心",)),
ScreenshotSpec("SS-12", "知识库文档可打开", "integrations", 1024, (("view", "knowledge"),), ("统一接入中心",)),
ScreenshotSpec("SS-13", "375px 手机设备页", "devices", 375, required_tokens=("手机设备",)),
ScreenshotSpec("SS-14", "真机写操作四证汇总", "devices", 1440, (("view", "evidence"),), ("手机设备",)),
ScreenshotSpec("SS-15", "第三方应用与凭证列表", "integrations", 1024, (("view", "apps"),), ("统一接入中心",)),
ScreenshotSpec("SS-16", "接口设备文档授权详情", "integrations", 1024, (("view", "grants"),), ("统一接入中心",)),
ScreenshotSpec("SS-17", "第三方用量与额度看板", "integrations", 1024, (("view", "usage"),), ("统一接入中心",)),
ScreenshotSpec("SS-18", "首页设备与微信运行摘要", "overview", 1440, (("view", "wechat-summary"),), ("数据总览",)),
)
def env_first(*names: str) -> str | None:
for name in names:
value = os.environ.get(name)
if value:
return value
return None
def build_url(base_url: str, tab: str, query: tuple[tuple[str, str], ...]) -> str:
parts = urlsplit(base_url)
existing = dict(parse_qsl(parts.query, keep_blank_values=True))
existing["tab"] = tab
existing.update(query)
return urlunsplit((parts.scheme, parts.netloc, parts.path or "/", urlencode(existing), parts.fragment))
def redact_url(url: str) -> str:
parts = urlsplit(url)
return urlunsplit((parts.scheme, parts.netloc, parts.path, "", ""))
def timestamp() -> str:
return datetime.now().astimezone().strftime("%Y%m%dT%H%M%S%z")
def safe_filename(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_.-]+", "-", value)
def find_browser(executable_path: str | None) -> str | None:
if executable_path:
return executable_path
candidates = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
]
for candidate in candidates:
if Path(candidate).is_file() and os.access(candidate, os.X_OK):
return candidate
for command in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
found = shutil.which(command)
if found and Path(found).is_file() and os.access(found, os.X_OK):
# Homebrew/旧环境可能留下只负责转发到已删除 App 的壳脚本。
try:
if Path(found).read_text(encoding="utf-8", errors="ignore").startswith("#!") and "exec '" in Path(found).read_text(encoding="utf-8", errors="ignore"):
continue
except OSError:
pass
return found
return None
def is_auth_request(url: str, login_path: str) -> bool:
return urlsplit(url).path.rstrip("/") == login_path.rstrip("/")
def image_guard(path: Path) -> tuple[bool, str]:
"""挡住全黑、全白或几乎没有内容的截图。"""
if Image is None:
return False, f"Pillow 不可用: {_PIL_IMPORT_ERROR}"
try:
with Image.open(path) as image:
rgb = image.convert("RGB")
stat = ImageStat.Stat(rgb.resize((64, 64)))
mean = sum(stat.mean) / 3
variance = sum(stat.var) / 3
if mean < 3:
return False, "black_page: 截图几乎全黑"
if mean > 252 and variance < 3:
return False, "blank_page: 截图几乎全白"
if variance < 1 and (mean < 8 or mean > 247):
return False, "blank_page: 截图近乎纯色"
return True, ""
except (OSError, ValueError) as exc:
return False, f"screenshot_unreadable: {exc}"
async def navigation_timing(page: Page, wall_ms: float) -> dict[str, Any]:
timing = await page.evaluate(
"""() => {
const navigation = performance.getEntriesByType('navigation')[0];
const paints = Object.fromEntries(
performance.getEntriesByType('paint').map(entry => [entry.name, entry.startTime])
);
return {
dom_content_loaded_ms: navigation?.domContentLoadedEventEnd || null,
load_event_ms: navigation?.loadEventEnd || null,
response_start_ms: navigation?.responseStart || null,
first_paint_ms: paints['first-paint'] || null,
first_contentful_paint_ms: paints['first-contentful-paint'] || null,
};
}"""
)
candidates = [
timing.get("first_contentful_paint_ms"),
timing.get("first_paint_ms"),
timing.get("dom_content_loaded_ms"),
timing.get("response_start_ms"),
wall_ms,
]
timing["first_screen_ms"] = round(float(next(value for value in candidates if value is not None)), 2)
return timing
async def page_text(page: Page) -> str:
try:
return (await page.locator("body").inner_text(timeout=1500)).strip()
except Exception:
return ""
async def validate_business_page(page: Page, spec: ScreenshotSpec) -> tuple[bool, str, str]:
text = await page_text(page)
lowered = text.lower()
if any(marker in text for marker in AUTH_MARKERS) and (await page.locator("input[type='password']").count() > 0):
return False, "login_page: 当前仍是登录页,未算业务页", text
if not text or len(text) < 80:
return False, "blank_page: 页面可见文本不足,未算业务页", text
if not any(marker in text for marker in BUSINESS_MARKERS):
return False, "not_business_page: 未找到总控平台业务页标识", text
missing = [token for token in spec.required_tokens if token not in text]
if missing:
return False, f"business_token_missing: 缺少页面标识 {', '.join(missing)}", text
if "error" in lowered and "登录服务暂时不可用" in text:
return False, "page_error: 页面显示登录服务异常", text
return True, "", text
async def maybe_login(page: Page, username: str | None, password: str | None, login_path: str) -> dict[str, Any]:
"""仅在显式提供账号密码时登录;不点任何业务操作。"""
result: dict[str, Any] = {"attempted": False, "succeeded": False, "reason": ""}
if not username or not password:
result["reason"] = "未提供登录环境变量"
return result
result["attempted"] = True
text = await page_text(page)
if "登录总控平台" not in text and await page.locator("input[type='password']").count() == 0:
result["succeeded"] = True
result["reason"] = "初始页面已是业务页"
return result
try:
user_input = page.locator("input[name='username'], input[autocomplete='username'], input[type='text']").first
password_input = page.locator("input[name='password'], input[autocomplete='current-password'], input[type='password']").first
await user_input.fill(username)
await password_input.fill(password)
await page.locator("button[type='submit'], button:has-text('登录')").first.click()
await page.wait_for_timeout(150)
result["succeeded"] = (await page.locator("input[type='password']").count()) == 0
result["reason"] = "登录完成" if result["succeeded"] else "登录后仍停留在登录页"
except Exception as exc:
result["reason"] = f"登录失败: {type(exc).__name__}: {exc}"
return result
async def capture_spec(
context: BrowserContext,
browser: Browser,
base_url: str,
spec: ScreenshotSpec,
output_dir: Path,
run_stamp: str,
timeout_ms: int,
failure_state: dict[str, Any],
) -> dict[str, Any]:
page = await context.new_page()
target_url = build_url(base_url, spec.tab, spec.query)
screenshot_dir = output_dir / "screenshots"
error_dir = output_dir / "errors"
screenshot_dir.mkdir(parents=True, exist_ok=True)
error_dir.mkdir(parents=True, exist_ok=True)
base_name = f"{spec.id}_{spec.width}x{VIEWPORTS[spec.width]}_{run_stamp}"
screenshot_path = screenshot_dir / f"{safe_filename(base_name)}.png"
error_path = error_dir / f"{safe_filename(base_name)}_error.png"
network_failures: list[dict[str, Any]] = []
response_failures: list[dict[str, Any]] = []
def record_failed_request(request: Any) -> None:
if request.resource_type in {"xhr", "fetch"} or "/api/" in request.url:
network_failures.append({"url": redact_url(request.url), "error": request.failure})
def record_response(response: Any) -> None:
if response.request.resource_type in {"xhr", "fetch"} or "/api/" in response.url:
if response.status >= 400:
response_failures.append({"url": redact_url(response.url), "status": response.status})
page.on("requestfailed", record_failed_request)
page.on("response", record_response)
started = time.perf_counter()
navigation_error = ""
try:
await page.goto(target_url, wait_until="domcontentloaded", timeout=timeout_ms)
await page.wait_for_timeout(120)
try:
await page.evaluate("document.fonts && document.fonts.ready")
except Exception:
pass
except Exception as exc:
navigation_error = f"navigation_error: {type(exc).__name__}: {exc}"
wall_ms = round((time.perf_counter() - started) * 1000, 2)
timing: dict[str, Any] = {"first_screen_ms": wall_ms}
if not navigation_error:
try:
timing = await navigation_timing(page, wall_ms)
except Exception as exc:
timing["timing_error"] = f"{type(exc).__name__}: {exc}"
ok, reason, text = (False, navigation_error, "") if navigation_error else await validate_business_page(page, spec)
try:
await page.screenshot(path=str(error_path if not ok else screenshot_path), full_page=True, timeout=timeout_ms)
except Exception as exc:
reason = f"{reason}; screenshot_error: {type(exc).__name__}: {exc}" if reason else f"screenshot_error: {type(exc).__name__}: {exc}"
image_path = error_path if not ok else screenshot_path
if image_path.exists():
image_ok, image_reason = image_guard(image_path)
if not image_ok:
ok = False
reason = f"{reason}; {image_reason}" if reason else image_reason
if image_path == screenshot_path:
try:
await page.screenshot(path=str(error_path), full_page=True, timeout=timeout_ms)
except Exception:
pass
interface_failures = network_failures + response_failures
if failure_state.get("readonly_violations"):
ok = False
reason = f"readonly_violation: 发现非认证写请求 {len(failure_state['readonly_violations'])}"
result = {
"id": spec.id,
"title": spec.title,
"url": redact_url(target_url),
"viewport": {"width": spec.width, "height": VIEWPORTS[spec.width]},
"status": "passed" if ok else "failed",
"reason": reason or "业务页标识、内容和截图检查通过",
"screenshot_path": str(screenshot_path.resolve()) if ok and screenshot_path.exists() else None,
"error_screenshot_path": str(error_path.resolve()) if (not ok and error_path.exists()) else None,
"screenshot_filename_contains_id_size_time": spec.id in image_path.name and str(spec.width) in image_path.name and run_stamp in image_path.name,
"visible_text_length": len(text),
"first_screen_ms": timing.get("first_screen_ms"),
"timing": timing,
"interface_failure_count": len(interface_failures),
"interface_failures": interface_failures,
"readonly_violation_count": len(failure_state.get("readonly_violations", [])),
}
await page.close()
return result
async def run_qa(args: argparse.Namespace) -> dict[str, Any]:
if _PLAYWRIGHT_IMPORT_ERROR:
raise RuntimeError(f"Playwright 不可用: {_PLAYWRIGHT_IMPORT_ERROR}")
output_dir = Path(args.output_dir).expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
run_stamp = timestamp()
base_url = args.base_url
username = args.username
password = args.password
token = args.auth_token
login_path = args.login_path
failure_state: dict[str, Any] = {"readonly_violations": [], "auth_requests": []}
async with async_playwright() as playwright:
executable_path = find_browser(args.executable_path)
browser = await playwright.chromium.launch(headless=not args.headed, executable_path=executable_path)
contexts: dict[int, BrowserContext] = {}
try:
for width, height in VIEWPORTS.items():
headers = {"Authorization": f"Bearer {token}"} if token else None
context = await browser.new_context(viewport={"width": width, "height": height}, extra_http_headers=headers)
async def guard_route(route: Any, request: Any) -> None:
if request.method in WRITE_METHODS:
if is_auth_request(request.url, login_path):
failure_state["auth_requests"].append({"method": request.method, "url": redact_url(request.url)})
await route.continue_()
return
failure_state["readonly_violations"].append({"method": request.method, "url": redact_url(request.url)})
await route.abort(error_code="blockedbyclient")
return
await route.continue_()
await context.route("**/*", guard_route)
contexts[width] = context
login_page = await context.new_page()
try:
await login_page.goto(base_url, wait_until="domcontentloaded", timeout=args.timeout_ms)
await login_page.wait_for_timeout(80)
await maybe_login(login_page, username, password, login_path)
finally:
await login_page.close()
results: list[dict[str, Any]] = []
for spec in SCREENSHOTS:
result = await capture_spec(
contexts[spec.width],
browser,
base_url,
spec,
output_dir,
run_stamp,
args.timeout_ms,
failure_state,
)
results.append(result)
finally:
for context in contexts.values():
await context.close()
await browser.close()
performance_by_viewport: dict[str, dict[str, Any]] = {}
for width in VIEWPORTS:
rows = [row for row in results if row["viewport"]["width"] == width]
timings = [row["first_screen_ms"] for row in rows if isinstance(row.get("first_screen_ms"), (int, float))]
performance_by_viewport[str(width)] = {
"viewport": {"width": width, "height": VIEWPORTS[width]},
"sample_count": len(rows),
"first_screen_ms": round(sum(timings) / len(timings), 2) if timings else None,
"first_screen_ms_max": max(timings) if timings else None,
"interface_failure_count": sum(row["interface_failure_count"] for row in rows),
}
counts = Counter(row["status"] for row in results)
report = {
"task_id": "WP-CONSOLE-13",
"task_name": "阿端·页面验收工具·一次跑完四种尺寸、性能和18张截图",
"run_mode": "local_read_only",
"generated_at": datetime.now().astimezone().isoformat(),
"base_url": redact_url(base_url),
"read_only": True,
"write_methods_blocked": sorted(WRITE_METHODS),
"auth_request_count": len(failure_state["auth_requests"]),
"readonly_violation_count": len(failure_state["readonly_violations"]),
"viewport_widths": list(VIEWPORTS),
"screenshot_spec_count": len(SCREENSHOTS),
"screenshot_success_count": counts["passed"],
"screenshot_failure_count": counts["failed"],
"business_page_rejection_count": sum(1 for row in results if row["status"] == "failed" and any(key in row["reason"] for key in ("blank_page", "black_page", "login_page", "not_business_page"))),
"interface_failure_count": sum(row["interface_failure_count"] for row in results),
"performance": performance_by_viewport,
"results": results,
}
(output_dir / "visual_qa_report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
performance_report = {
"task_id": report["task_id"],
"generated_at": report["generated_at"],
"first_screen_time_definition": "优先 First Contentful Paint其次 First Paint、DOMContentLoaded、responseStart 或导航墙钟时间",
"interface_failure_count": report["interface_failure_count"],
"by_viewport": performance_by_viewport,
}
(output_dir / "performance_report.json").write_text(json.dumps(performance_report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
manifest = {
"task_id": report["task_id"],
"generated_at": report["generated_at"],
"screenshots": [
{
"id": row["id"],
"title": row["title"],
"viewport": row["viewport"],
"status": row["status"],
"screenshot_path": row["screenshot_path"],
"error_screenshot_path": row["error_screenshot_path"],
"reason": row["reason"],
}
for row in results
],
}
(output_dir / "screenshot_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
md_lines = [
"# WP-CONSOLE-13 页面视觉与性能验收报告",
"",
"- 运行模式:本地只读;写方法已拦截,未调用手机,未执行业务写接口。",
f"- 页面入口:`{report['base_url']}`",
f"- 四档尺寸:{', '.join(f'{w}px' for w in VIEWPORTS)};截图规格:{report['screenshot_spec_count']} 张。",
f"- 截图结果:成功 {report['screenshot_success_count']},失败 {report['screenshot_failure_count']};接口失败数:{report['interface_failure_count']}",
"",
"## 性能",
"",
"| 宽度 | 样本数 | 平均首屏(ms) | 最大首屏(ms) | 接口失败数 |",
"|---:|---:|---:|---:|---:|",
]
for width, item in performance_by_viewport.items():
md_lines.append(f"| {width} | {item['sample_count']} | {item['first_screen_ms']} | {item['first_screen_ms_max']} | {item['interface_failure_count']} |")
md_lines.extend(["", "## 截图清单", "", "| 编号 | 尺寸 | 结果 | 成功截图 | 失败截图 | 原因 |", "|---|---:|---|---|---|---|"])
for row in results:
md_lines.append(f"| {row['id']} | {row['viewport']['width']}x{row['viewport']['height']} | {row['status']} | {row['screenshot_path'] or ''} | {row['error_screenshot_path'] or ''} | {row['reason']} |")
(output_dir / "visual_qa_report.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
return report
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="工作手机总控平台四档页面视觉与性能验收")
parser.add_argument("--base-url", default=env_first("BASE_URL", "CONSOLE_QA_BASE_URL") or "http://127.0.0.1:8899/")
parser.add_argument("--output-dir", type=Path, default=Path(env_first("CONSOLE_QA_OUTPUT_DIR") or DEFAULT_OUTPUT_DIR))
parser.add_argument("--username", default=env_first("CONSOLE_QA_USERNAME", "CONSOLE_USERNAME", "USERNAME"))
parser.add_argument("--password", default=env_first("CONSOLE_QA_PASSWORD", "CONSOLE_PASSWORD", "PASSWORD"))
parser.add_argument("--auth-token", default=env_first("CONSOLE_QA_AUTH_TOKEN", "CONSOLE_AUTH_TOKEN", "AUTH_TOKEN"))
parser.add_argument("--login-path", default=env_first("CONSOLE_QA_LOGIN_PATH", "LOGIN_PATH") or "/api/v3/console/login")
parser.add_argument("--timeout-ms", type=int, default=int(env_first("CONSOLE_QA_TIMEOUT_MS") or "15000"))
parser.add_argument("--executable-path", default=env_first("CONSOLE_QA_BROWSER") or None)
parser.add_argument("--headed", action="store_true", help="显示浏览器窗口;默认无界面")
parser.add_argument("--strict", action="store_true", help="存在截图失败、写请求或接口失败时返回非零")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
try:
report = asyncio.run(run_qa(args))
except Exception as exc:
print(f"视觉验收工具执行失败:{type(exc).__name__}: {exc}", file=sys.stderr)
return 3
summary = {
"task_id": report["task_id"],
"run_mode": report["run_mode"],
"viewport_widths": report["viewport_widths"],
"screenshot_spec_count": report["screenshot_spec_count"],
"screenshot_success_count": report["screenshot_success_count"],
"screenshot_failure_count": report["screenshot_failure_count"],
"interface_failure_count": report["interface_failure_count"],
"readonly_violation_count": report["readonly_violation_count"],
"output_dir": str(Path(args.output_dir).expanduser().resolve()),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
if args.strict and (report["screenshot_failure_count"] or report["interface_failure_count"] or report["readonly_violation_count"]):
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

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

View File

@@ -0,0 +1,48 @@
{
"task_id": "WP-CONSOLE-13",
"generated_at": "2026-08-09T14:03:54.036050+08:00",
"first_screen_time_definition": "优先 First Contentful Paint其次 First Paint、DOMContentLoaded、responseStart 或导航墙钟时间",
"interface_failure_count": 0,
"by_viewport": {
"1440": {
"viewport": {
"width": 1440,
"height": 1200
},
"sample_count": 4,
"first_screen_ms": 216.0,
"first_screen_ms_max": 280.0,
"interface_failure_count": 0
},
"1024": {
"viewport": {
"width": 1024,
"height": 1100
},
"sample_count": 13,
"first_screen_ms": 196.92,
"first_screen_ms_max": 288.0,
"interface_failure_count": 0
},
"768": {
"viewport": {
"width": 768,
"height": 1024
},
"sample_count": 0,
"first_screen_ms": null,
"first_screen_ms_max": null,
"interface_failure_count": 0
},
"375": {
"viewport": {
"width": 375,
"height": 900
},
"sample_count": 1,
"first_screen_ms": 176.0,
"first_screen_ms_max": 176.0,
"interface_failure_count": 0
}
}
}

View File

@@ -0,0 +1,222 @@
{
"task_id": "WP-CONSOLE-13",
"generated_at": "2026-08-09T14:03:54.036050+08:00",
"screenshots": [
{
"id": "SS-01",
"title": "数据总览 1440px",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-02",
"title": "数据总览接口异常/数据过期",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-03",
"title": "手机设备含下载与扫码按钮",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-04",
"title": "添加设备 APK 下载步骤",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-05",
"title": "添加设备原扫码绑定步骤",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-06",
"title": "设备卡片三种状态",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-07",
"title": "单机控制台概览",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-08",
"title": "设备分组管理",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-09",
"title": "在线设备禁止删除",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-10",
"title": "智能引擎四模块",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-11",
"title": "统一接入中心接口目录",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-12",
"title": "知识库文档可打开",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-13",
"title": "375px 手机设备页",
"viewport": {
"width": 375,
"height": 900
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-14",
"title": "真机写操作四证汇总",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-15",
"title": "第三方应用与凭证列表",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-16",
"title": "接口设备文档授权详情",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-17",
"title": "第三方用量与额度看板",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
},
{
"id": "SS-18",
"title": "首页设备与微信运行摘要",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"reason": "业务页标识、内容和截图检查通过"
}
]
}

View File

@@ -0,0 +1,557 @@
{
"task_id": "WP-CONSOLE-13",
"task_name": "阿端·页面验收工具·一次跑完四种尺寸、性能和18张截图",
"run_mode": "local_read_only",
"generated_at": "2026-08-09T14:03:54.036050+08:00",
"base_url": "http://127.0.0.1:50406/",
"read_only": true,
"write_methods_blocked": [
"DELETE",
"PATCH",
"POST",
"PUT"
],
"auth_request_count": 0,
"readonly_violation_count": 0,
"viewport_widths": [
1440,
1024,
768,
375
],
"screenshot_spec_count": 18,
"screenshot_success_count": 18,
"screenshot_failure_count": 0,
"business_page_rejection_count": 0,
"interface_failure_count": 0,
"performance": {
"1440": {
"viewport": {
"width": 1440,
"height": 1200
},
"sample_count": 4,
"first_screen_ms": 216.0,
"first_screen_ms_max": 280.0,
"interface_failure_count": 0
},
"1024": {
"viewport": {
"width": 1024,
"height": 1100
},
"sample_count": 13,
"first_screen_ms": 196.92,
"first_screen_ms_max": 288.0,
"interface_failure_count": 0
},
"768": {
"viewport": {
"width": 768,
"height": 1024
},
"sample_count": 0,
"first_screen_ms": null,
"first_screen_ms_max": null,
"interface_failure_count": 0
},
"375": {
"viewport": {
"width": 375,
"height": 900
},
"sample_count": 1,
"first_screen_ms": 176.0,
"first_screen_ms_max": 176.0,
"interface_failure_count": 0
}
},
"results": [
{
"id": "SS-01",
"title": "数据总览 1440px",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 252.0,
"timing": {
"dom_content_loaded_ms": 217.79999999701977,
"load_event_ms": 218.19999999552965,
"response_start_ms": 42.599999994039536,
"first_paint_ms": 120,
"first_contentful_paint_ms": 252,
"first_screen_ms": 252.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-02",
"title": "数据总览接口异常/数据过期",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 160.0,
"timing": {
"dom_content_loaded_ms": 210.80000000447035,
"load_event_ms": 211.30000000447035,
"response_start_ms": 30.600000001490116,
"first_paint_ms": 160,
"first_contentful_paint_ms": 160,
"first_screen_ms": 160.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-03",
"title": "手机设备含下载与扫码按钮",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 200.0,
"timing": {
"dom_content_loaded_ms": 181.89999999850988,
"load_event_ms": 182.39999999850988,
"response_start_ms": 31.899999998509884,
"first_paint_ms": 200,
"first_contentful_paint_ms": 200,
"first_screen_ms": 200.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-04",
"title": "添加设备 APK 下载步骤",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 168.0,
"timing": {
"dom_content_loaded_ms": 241.90000000596046,
"load_event_ms": 242.5,
"response_start_ms": 49.5,
"first_paint_ms": 168,
"first_contentful_paint_ms": 168,
"first_screen_ms": 168.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-05",
"title": "添加设备原扫码绑定步骤",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 252.0,
"timing": {
"dom_content_loaded_ms": 216.20000000298023,
"load_event_ms": 216.5,
"response_start_ms": 34.5,
"first_paint_ms": 120,
"first_contentful_paint_ms": 252,
"first_screen_ms": 252.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-06",
"title": "设备卡片三种状态",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 148.0,
"timing": {
"dom_content_loaded_ms": 221.60000000149012,
"load_event_ms": 222.10000000149012,
"response_start_ms": 30.700000002980232,
"first_paint_ms": 148,
"first_contentful_paint_ms": 148,
"first_screen_ms": 148.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-07",
"title": "单机控制台概览",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 164.0,
"timing": {
"dom_content_loaded_ms": 222.5,
"load_event_ms": 223,
"response_start_ms": 31.799999997019768,
"first_paint_ms": 164,
"first_contentful_paint_ms": 164,
"first_screen_ms": 164.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-08",
"title": "设备分组管理",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 168.0,
"timing": {
"dom_content_loaded_ms": 188.20000000298023,
"load_event_ms": 188.70000000298023,
"response_start_ms": 28.899999998509884,
"first_paint_ms": 168,
"first_contentful_paint_ms": 168,
"first_screen_ms": 168.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-09",
"title": "在线设备禁止删除",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 152.0,
"timing": {
"dom_content_loaded_ms": 214.80000000447035,
"load_event_ms": 215.39999999850988,
"response_start_ms": 29.399999998509884,
"first_paint_ms": 152,
"first_contentful_paint_ms": 152,
"first_screen_ms": 152.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-10",
"title": "智能引擎四模块",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 148.0,
"timing": {
"dom_content_loaded_ms": 209.80000000447035,
"load_event_ms": 210.20000000298023,
"response_start_ms": 28.30000000447035,
"first_paint_ms": 148,
"first_contentful_paint_ms": 148,
"first_screen_ms": 148.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-11",
"title": "统一接入中心接口目录",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 156.0,
"timing": {
"dom_content_loaded_ms": 210.89999999850988,
"load_event_ms": 211.39999999850988,
"response_start_ms": 27.899999998509884,
"first_paint_ms": 156,
"first_contentful_paint_ms": 156,
"first_screen_ms": 156.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-12",
"title": "知识库文档可打开",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 152.0,
"timing": {
"dom_content_loaded_ms": 216.39999999850988,
"load_event_ms": 216.89999999850988,
"response_start_ms": 32.600000001490116,
"first_paint_ms": 152,
"first_contentful_paint_ms": 152,
"first_screen_ms": 152.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-13",
"title": "375px 手机设备页",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 375,
"height": 900
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 176.0,
"timing": {
"dom_content_loaded_ms": 192.69999999552965,
"load_event_ms": 193.10000000149012,
"response_start_ms": 35,
"first_paint_ms": 176,
"first_contentful_paint_ms": 176,
"first_screen_ms": 176.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-14",
"title": "真机写操作四证汇总",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 280.0,
"timing": {
"dom_content_loaded_ms": 237.10000000149012,
"load_event_ms": 237.80000000447035,
"response_start_ms": 42.900000005960464,
"first_paint_ms": 132,
"first_contentful_paint_ms": 280,
"first_screen_ms": 280.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-15",
"title": "第三方应用与凭证列表",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 288.0,
"timing": {
"dom_content_loaded_ms": 247.89999999850988,
"load_event_ms": 248.29999999701977,
"response_start_ms": 45.70000000298023,
"first_paint_ms": 140,
"first_contentful_paint_ms": 288,
"first_screen_ms": 288.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-16",
"title": "接口设备文档授权详情",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 288.0,
"timing": {
"dom_content_loaded_ms": 257.80000000447035,
"load_event_ms": 258.5,
"response_start_ms": 47.399999998509884,
"first_paint_ms": 140,
"first_contentful_paint_ms": 288,
"first_screen_ms": 288.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-17",
"title": "第三方用量与额度看板",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1024,
"height": 1100
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 276.0,
"timing": {
"dom_content_loaded_ms": 244.39999999850988,
"load_event_ms": 245,
"response_start_ms": 41,
"first_paint_ms": 128,
"first_contentful_paint_ms": 276,
"first_screen_ms": 276.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
},
{
"id": "SS-18",
"title": "首页设备与微信运行摘要",
"url": "http://127.0.0.1:50406/",
"viewport": {
"width": 1440,
"height": 1200
},
"status": "passed",
"reason": "业务页标识、内容和截图检查通过",
"screenshot_path": "/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png",
"error_screenshot_path": null,
"screenshot_filename_contains_id_size_time": false,
"visible_text_length": 151,
"first_screen_ms": 172.0,
"timing": {
"dom_content_loaded_ms": 230.10000000149012,
"load_event_ms": 230.70000000298023,
"response_start_ms": 37.30000000447035,
"first_paint_ms": 172,
"first_contentful_paint_ms": 172,
"first_screen_ms": 172.0
},
"interface_failure_count": 0,
"interface_failures": [],
"readonly_violation_count": 0
}
]
}

View File

@@ -0,0 +1,38 @@
# WP-CONSOLE-13 页面视觉与性能验收报告
- 运行模式:本地只读;写方法已拦截,未调用手机,未执行业务写接口。
- 页面入口:`http://127.0.0.1:50406/`
- 四档尺寸1440px, 1024px, 768px, 375px截图规格18 张。
- 截图结果:成功 18失败 0接口失败数0。
## 性能
| 宽度 | 样本数 | 平均首屏(ms) | 最大首屏(ms) | 接口失败数 |
|---:|---:|---:|---:|---:|
| 1440 | 4 | 216.0 | 280.0 | 0 |
| 1024 | 13 | 196.92 | 288.0 | 0 |
| 768 | 0 | None | None | 0 |
| 375 | 1 | 176.0 | 176.0 | 0 |
## 截图清单
| 编号 | 尺寸 | 结果 | 成功截图 | 失败截图 | 原因 |
|---|---:|---|---|---|---|
| SS-01 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-01_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-02 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-02_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-03 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-03_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-04 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-04_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-05 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-05_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-06 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-06_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-07 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-07_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-08 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-08_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-09 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-09_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-10 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-10_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-11 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-11_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-12 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-12_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-13 | 375x900 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-13_375x900_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-14 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-14_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-15 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-15_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-16 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-16_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-17 | 1024x1100 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-17_1024x1100_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |
| SS-18 | 1440x1200 | passed | /Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/10、项目管理/02-测试报告/20260809_WP_CONSOLE_13_VISUAL_QA/screenshots/SS-18_1440x1200_20260809T140326-0800.png | | 业务页标识、内容和截图检查通过 |