Skip to content
Source
Sign inSign up
Open History to see the last commit

report.ts

96 lines4.6 KB

1// @effect-diagnostics nodeBuiltinImport:off - Writes a static HTML report.
2​
3import * as NodePath from "node:path";
4​
5import { poolError } from "./device.ts";
6import type { StepResult } from "./journey.ts";
7import type { Pose } from "./poses.ts";
8​
9export interface Finding {
10 readonly step: string;
11 readonly issue: string;
12 readonly severity: "high" | "medium" | "low";
14​
15export interface PoseResult {
16 readonly pose: Pose;
17 readonly steps: ReadonlyArray<StepResult>;
18 readonly error?: string;
19 /** Screenshot taken when the journey stopped early. */
20 readonly stopped?: string;
21 readonly findings?: ReadonlyArray<Finding>;
23​
24export const failedChecks = (result: PoseResult) =>
25 result.steps.flatMap((step) =>
26 step.checks.filter((item) => !item.pass).map((item) => ({ step: step.name, ...item })),
27 );
28​
29export const poseFailed = (result: PoseResult) =>
30 result.error !== undefined || failedChecks(result).length > 0;
31​
32/** The pool never let the pose finish, so it says nothing about the app. */
33export const poolBlocked = (result: PoseResult) =>
34 result.error !== undefined && poolError(result.error) && failedChecks(result).length === 0;
35​
36const escapeHtml = (value: string) =>
37 value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
38​
39const describePose = (pose: Pose) =>
40 [
41 pose.device,
42 pose.appearance,
43 pose.landscape ? "landscape" : "",
44 pose.fold ? `fold ${pose.fold}` : "",
45 pose.textSize ? "large text" : "",
46 ]
47 .filter(Boolean)
48 .join(" · ");
49​
50export function renderReport(results: ReadonlyArray<PoseResult>, outputDirectory: string): string {
51 const relative = (path: string | undefined) =>
52 path ? escapeHtml(NodePath.relative(outputDirectory, path)) : "";
53 const rows = results
54 .map((result) => {
55 const cells = result.steps
56 .map((step) => {
57 const failed = step.checks.filter((item) => !item.pass);
58 const findings = (result.findings ?? []).filter((finding) => finding.step === step.name);
59 const notes = [
60 ...failed.map(
61 (item) =>
62 `<li class="fail">✗ ${escapeHtml(item.name)} <small>${escapeHtml(item.detail)}</small></li>`,
63 ),
64 ...findings.map(
65 (finding) =>
66 `<li class="review ${finding.severity}">◆ ${escapeHtml(finding.issue)}</li>`,
67 ),
68 ].join("");
69 const passed = step.checks.length - failed.length;
70 const media = [
71 step.screenshot
72 ? `<a href="${relative(step.screenshot)}"><img src="${relative(step.screenshot)}"></a>`
73 : "",
74 step.lagFrame
75 ? `<a href="${relative(step.lagFrame)}"><img class="lag" src="${relative(step.lagFrame)}"></a>`
76 : "",
77 step.video ? `<a href="${relative(step.video)}">video</a>` : "",
78 ].join("");
79 return `<td class="${failed.length ? "bad" : ""}"><h3>${escapeHtml(step.name)}</h3>${media}<small class="pass">${passed}/${step.checks.length} checks</small><ul>${notes}</ul></td>`;
80 })
81 .join("");
82 const error = result.error
83 ? `<td class="bad"><h3>${poolBlocked(result) ? "blocked by the pool" : "stopped"}</h3>${result.stopped ? `<a href="${relative(result.stopped)}"><img src="${relative(result.stopped)}"></a>` : ""}<p class="fail">${escapeHtml(result.error)}</p></td>`
84 : "";
85 return `<tr><th>${escapeHtml(result.pose.id)}<br><small>${escapeHtml(describePose(result.pose))}</small></th>${cells}${error}</tr>`;
86 })
87 .join("");
88 const failed = results.filter((result) => poseFailed(result) && !poolBlocked(result)).length;
89 const blocked = results.filter(poolBlocked).length;
90 const findings = results.reduce((count, result) => count + (result.findings?.length ?? 0), 0);
91 return `<!doctype html><meta charset="utf-8"><title>Mobile E2E</title>
92<style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:8px;vertical-align:top;text-align:left}td{width:190px}td.bad{background:#fff5f4}h3{margin:0 0 6px;font-size:13px}img{width:180px;display:block;margin:0 0 6px}img.lag{outline:3px solid #b42318}.pass{color:#16794a}.fail{color:#b42318}.review{color:#7a4b00}.review.high{color:#b42318}ul{padding-left:16px;margin:4px 0}small{color:#666}</style>
93<h1>Mobile E2E · ${results.length} poses · ${failed === 0 ? "all checks passed" : `${failed} with failing checks`}${blocked ? ` · ${blocked} blocked by the pool` : ""} · ${findings} review findings</h1>
94<p><span class="fail">✗</span> failed check <span class="review">◆</span> agent review finding</p>
95<table>${rows}</table>`;