generate_html_report
Renders last test results into a self-contained HTML report with embedded screenshots, step lists, and history sparkline, collapsing passed sections and expanding failed cards. No external dependencies needed.
Instructions
把最近一次 run_tests 的結果渲染成單檔自包含 HTML——base64 內嵌截圖、嵌入式 step list、history sparkline 走勢、折疊的 Passed 區塊、展開的 Failed cards。沒外部 CSS/JS 依賴,可以直接寄信、丟靜態 host、貼到 Slack。預設輸出 PROJECT_ROOT/report.html。實作位於 reporters/html.py,走 sample_report.html 同款設計。
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| output | No | 選填,輸出檔名(相對於 QA_PROJECT_ROOT)。預設 `report.html`。 | report.html |
Implementation Reference
- src/mk_qa_master/server.py:280-302 (registration)The tool 'generate_html_report' is registered in the list_tools() function with its name, description, and input schema.
Tool( name="generate_html_report", description=( "把最近一次 run_tests 的結果渲染成單檔自包含 HTML——base64 內嵌截圖、" "嵌入式 step list、history sparkline 走勢、折疊的 Passed 區塊、展開的 Failed cards。" "沒外部 CSS/JS 依賴,可以直接寄信、丟靜態 host、貼到 Slack。" "預設輸出 PROJECT_ROOT/report.html。實作位於 reporters/html.py," "走 sample_report.html 同款設計。" ), inputSchema={ "type": "object", "properties": { "output": { "type": "string", "default": "report.html", "description": ( "選填,輸出檔名(相對於 QA_PROJECT_ROOT)。" "預設 `report.html`。" ), }, }, }, ), - src/mk_qa_master/server.py:694-696 (handler)The handler function that dispatches the 'generate_html_report' tool call to html_reporter.write_report().
if name == "generate_html_report": target = html_reporter.write_report(args.get("output", "report.html")) return [TextContent(type="text", text=f"已產生 HTML 報告:{target}")] - The write_report() helper function that renders the HTML and writes it to disk.
def write_report(output: str = "report.html") -> Path: """Render and write the HTML report to disk under PROJECT_ROOT.""" target = PROJECT_ROOT / output target.parent.mkdir(parents=True, exist_ok=True) target.write_text(render_report(), encoding="utf-8") return target - The render_report() function that builds the full self-contained HTML report by gathering test data from the runner and applying template replacements.
def render_report() -> str: """Render the latest test report as a self-contained HTML string.""" runner = get_runner() summary = runner.get_report_summary() # Prefer the rich details (with steps) when available; fall back to the # legacy failure-only path so non-pytest runners still render. all_details = runner.get_all_test_details() if hasattr(runner, "get_all_test_details") else [] if all_details: failures = [t for t in all_details if t.get("outcome") == "failed"] passes = [t for t in all_details if t.get("outcome") == "passed"] else: failures = runner.get_failure_details() or [] passes = [] has_error = isinstance(summary, dict) and "error" in summary total = int(summary.get("total", 0) or 0) if not has_error else 0 passed = int(summary.get("passed", 0) or 0) if not has_error else 0 failed = int(summary.get("failed", 0) or 0) if not has_error else 0 skipped = int(summary.get("skipped", 0) or 0) if not has_error else 0 flaky_in_run = int(summary.get("flaky_in_run", 0) or 0) if not has_error else 0 duration = summary.get("duration") if not has_error else None pass_rate = (passed / total * 100) if total else 0 duration_str = f"{duration:.2f}s" if isinstance(duration, (int, float)) else "—" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if has_error: failure_html = f'<div class="empty">{escape(str(summary.get("error", "找不到報告")))}</div>' elif failed == 0 and total > 0: failure_html = '<div class="empty success">所有測試通過</div>' elif failed == 0 and total == 0: failure_html = '<div class="empty">尚未執行任何測試</div>' else: cards = [] for i, f in enumerate(failures or []): if isinstance(f, dict) and "error" in f: continue nodeid = escape(str(f.get("nodeid", "unknown"))) message = escape(str(f.get("message", ""))) dur = f.get("duration") dur_str = f"{dur:.3f}s" if isinstance(dur, (int, float)) else "" open_attr = "open" if i < 3 else "" shot_html = _embed_screenshot(f.get("screenshot") if isinstance(f, dict) else None) links_html = _artifact_links(f if isinstance(f, dict) else {}) steps_html = _render_steps_html(f.get("steps") or []) if isinstance(f, dict) else "" meta_html = _render_test_meta( f.get("title") if isinstance(f, dict) else None, f.get("nodeid", "unknown") if isinstance(f, dict) else "unknown", ) cards.append( f'<details class="failure" {open_attr}>' f'<summary>' f'<span class="fail-badge">FAIL</span>' f'{meta_html}' f'<span class="fail-dur">{dur_str}</span>' f'</summary>' f'<pre>{message}</pre>' f'{steps_html}' f'{shot_html}' f'{links_html}' f'</details>' ) failure_html = "\n".join(cards) if cards else '<div class="empty">無失敗詳情</div>' history = runner.get_history(limit=10) if hasattr(runner, "get_history") else [] trend_html = _render_trend(history, failed) passed_html = _render_pass_section(passes) flaky_stat_html = ( f'<div class="stat flaky"><div class="label">Flaky (retried)</div>' f'<div class="value">{flaky_in_run}</div></div>' if flaky_in_run > 0 else "" ) out = TEMPLATE replacements = { "{{RUNNER_NAME}}": escape(runner.name), "{{TIMESTAMP}}": timestamp, "{{PROJECT_ROOT}}": escape(str(PROJECT_ROOT)), "{{TOTAL}}": str(total), "{{PASSED}}": str(passed), "{{FAILED}}": str(failed), "{{SKIPPED}}": str(skipped), "{{FLAKY_STAT}}": flaky_stat_html, "{{DURATION}}": duration_str, "{{PASS_RATE}}": f"{pass_rate:.1f}", "{{PASS_RATE_INT}}": str(int(pass_rate)), "{{TREND_SECTION}}": trend_html, "{{FAILURE_SECTION}}": failure_html, "{{PASSED_SECTION}}": passed_html, "{{YEAR}}": str(datetime.now().year), } for k, v in replacements.items(): out = out.replace(k, v) return out - src/mk_qa_master/server.py:289-301 (schema)The input schema for the generate_html_report tool, accepting an optional 'output' string parameter.
inputSchema={ "type": "object", "properties": { "output": { "type": "string", "default": "report.html", "description": ( "選填,輸出檔名(相對於 QA_PROJECT_ROOT)。" "預設 `report.html`。" ), }, }, },