feat(report): embed screenshots as base64 in report_cn.html with click-to-zoom preview

- Convert PNG screenshots to base64 and embed directly in HTML
- Grid gallery layout with thumbnail previews
- Click to expand/zoom to fullscreen overlays
- No external file dependencies — report is self-contained
This commit is contained in:
xst
2026-07-23 17:59:42 +08:00
parent 51768c7c95
commit f23051a876
+35 -10
View File
@@ -293,13 +293,22 @@ def _collect_logs(report_dir: Path) -> list:
def _collect_screenshots() -> list: def _collect_screenshots() -> list:
"""收集测试过程中生成的截图。""" """收集测试过程中生成的截图,转为 base64 内嵌到 HTML 中"""
import base64
shots = [] shots = []
ss_dir = Path("output/screenshots") ss_dir = Path("output/screenshots")
if ss_dir.exists(): if ss_dir.exists():
for p in sorted(ss_dir.rglob("*.png"), reverse=True)[:50]: for p in sorted(ss_dir.rglob("*.png"), reverse=True)[:30]:
# 用相对路径 try:
shots.append(str(p).replace("\\", "/")) data = p.read_bytes()
b64 = base64.b64encode(data).decode("ascii")
shots.append({
"name": p.name,
"path": str(p).replace("\\", "/"),
"b64": b64,
})
except Exception:
pass
return shots return shots
@@ -383,11 +392,19 @@ def _build_cn_report_html(total, passed, failed, skipped, rate, cases,
# ── 截图面板 ── # ── 截图面板 ──
screenshot_html = "" screenshot_html = ""
if screenshots: if screenshots:
shot_items = "".join(f'<div class="shot-item" title="{s}">📷 {s.split("/")[-1]}</div>' for s in screenshots[:30]) shot_cards = ""
for s in screenshots[:20]:
shot_cards += f"""
<div class="shot-card">
<div class="shot-thumb" onclick="this.classList.toggle('expanded')">
<img src="data:image/png;base64,{s['b64']}" loading="lazy" alt="{s['name']}">
</div>
<div class="shot-label">{s['name']}</div>
</div>"""
screenshot_html = f""" screenshot_html = f"""
<details class="log-section"> <details class="log-section" open>
<summary>📸 截图列表 ({len(screenshots)} 张)</summary> <summary>📸 测试截图 ({len(screenshots)} 张) — 点击可放大</summary>
<div class="shot-grid">{shot_items}</div> <div class="shot-gallery">{shot_cards}</div>
</details>""" </details>"""
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
@@ -432,8 +449,16 @@ tr:hover td{{background:#f5f7fa}}
.log-section{{margin:0 32px 12px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:12px 20px}} .log-section{{margin:0 32px 12px;background:#fff;border-radius:8px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:12px 20px}}
.log-section summary{{font-size:14px;font-weight:500;cursor:pointer;padding:4px 0}} .log-section summary{{font-size:14px;font-weight:500;cursor:pointer;padding:4px 0}}
.log-content{{font-size:11px;background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:4px;white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto;font-family:'Consolas','Courier New',monospace}} .log-content{{font-size:11px;background:#1e1e1e;color:#d4d4d4;padding:12px;border-radius:4px;white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto;font-family:'Consolas','Courier New',monospace}}
.shot-grid{{display:flex;flex-wrap:wrap;gap:8px;padding:8px 0}} .shot-gallery{{display:flex;flex-wrap:wrap;gap:16px;padding:8px 0}}
.shot-item{{font-size:12px;color:#606266;background:#f5f7fa;padding:4px 10px;border-radius:4px}} .shot-card{{width:280px;border-radius:8px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.08);background:#fff;transition:box-shadow .2s}}
.shot-card:hover{{box-shadow:0 4px 16px rgba(0,0,0,.15)}}
.shot-thumb{{width:100%;height:200px;overflow:hidden;cursor:pointer;position:relative;background:#f5f7fa}}
.shot-thumb img{{width:100%;height:100%;object-fit:contain;display:block}}
.shot-thumb.expanded{{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;background:rgba(0,0,0,.85)}}
.shot-thumb.expanded img{{width:auto;height:auto;max-width:100%;max-height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}}
.shot-thumb::after{{content:'🔍 点击放大';position:absolute;bottom:0;left:0;right:0;padding:6px;background:rgba(0,0,0,.5);color:#fff;font-size:11px;text-align:center;opacity:0;transition:opacity .2s}}
.shot-thumb:hover::after{{opacity:1}}
.shot-label{{font-size:11px;color:#606266;padding:6px 10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}}
.ft{{text-align:center;padding:24px;font-size:12px;color:#c0c4cc}} .ft{{text-align:center;padding:24px;font-size:12px;color:#c0c4cc}}
</style> </style>
</head> </head>