@monoes/monobrowse 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/__tests__/bridge-instruments.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-instruments.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-instruments.test.js +132 -0
- package/dist/src/__tests__/bridge-instruments.test.js.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts +2 -0
- package/dist/src/__tests__/bridge-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/bridge-transport.test.js +287 -0
- package/dist/src/__tests__/bridge-transport.test.js.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts +2 -0
- package/dist/src/__tests__/cdp-transport.test.d.ts.map +1 -0
- package/dist/src/__tests__/cdp-transport.test.js +167 -0
- package/dist/src/__tests__/cdp-transport.test.js.map +1 -0
- package/dist/src/__tests__/profiler-heap.test.d.ts +2 -0
- package/dist/src/__tests__/profiler-heap.test.d.ts.map +1 -0
- package/dist/src/__tests__/profiler-heap.test.js +154 -0
- package/dist/src/__tests__/profiler-heap.test.js.map +1 -0
- package/dist/src/__tests__/report-a11y.test.d.ts +2 -0
- package/dist/src/__tests__/report-a11y.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-a11y.test.js +147 -0
- package/dist/src/__tests__/report-a11y.test.js.map +1 -0
- package/dist/src/__tests__/report-analyze.test.d.ts +2 -0
- package/dist/src/__tests__/report-analyze.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-analyze.test.js +157 -0
- package/dist/src/__tests__/report-analyze.test.js.map +1 -0
- package/dist/src/__tests__/report-budget.test.d.ts +2 -0
- package/dist/src/__tests__/report-budget.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-budget.test.js +80 -0
- package/dist/src/__tests__/report-budget.test.js.map +1 -0
- package/dist/src/__tests__/report-diff.test.d.ts +2 -0
- package/dist/src/__tests__/report-diff.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-diff.test.js +231 -0
- package/dist/src/__tests__/report-diff.test.js.map +1 -0
- package/dist/src/__tests__/report-evidence.test.d.ts +2 -0
- package/dist/src/__tests__/report-evidence.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-evidence.test.js +214 -0
- package/dist/src/__tests__/report-evidence.test.js.map +1 -0
- package/dist/src/__tests__/report-flake.test.d.ts +2 -0
- package/dist/src/__tests__/report-flake.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-flake.test.js +182 -0
- package/dist/src/__tests__/report-flake.test.js.map +1 -0
- package/dist/src/__tests__/report-history.test.d.ts +2 -0
- package/dist/src/__tests__/report-history.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-history.test.js +229 -0
- package/dist/src/__tests__/report-history.test.js.map +1 -0
- package/dist/src/__tests__/report-output.test.d.ts +2 -0
- package/dist/src/__tests__/report-output.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-output.test.js +169 -0
- package/dist/src/__tests__/report-output.test.js.map +1 -0
- package/dist/src/__tests__/report-png.test.d.ts +2 -0
- package/dist/src/__tests__/report-png.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-png.test.js +84 -0
- package/dist/src/__tests__/report-png.test.js.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts +2 -0
- package/dist/src/__tests__/report-render-extras.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render-extras.test.js +285 -0
- package/dist/src/__tests__/report-render-extras.test.js.map +1 -0
- package/dist/src/__tests__/report-render.test.d.ts +2 -0
- package/dist/src/__tests__/report-render.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-render.test.js +143 -0
- package/dist/src/__tests__/report-render.test.js.map +1 -0
- package/dist/src/__tests__/report-run.test.d.ts +2 -0
- package/dist/src/__tests__/report-run.test.d.ts.map +1 -0
- package/dist/src/__tests__/report-run.test.js +234 -0
- package/dist/src/__tests__/report-run.test.js.map +1 -0
- package/dist/src/browser/bridge.d.ts +119 -0
- package/dist/src/browser/bridge.d.ts.map +1 -0
- package/dist/src/browser/bridge.js +264 -0
- package/dist/src/browser/bridge.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +11 -1
- package/dist/src/browser/cdp.d.ts.map +1 -1
- package/dist/src/browser/cdp.js +60 -56
- package/dist/src/browser/cdp.js.map +1 -1
- package/dist/src/browser/index.d.ts +3 -0
- package/dist/src/browser/index.d.ts.map +1 -1
- package/dist/src/browser/index.js +3 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/profiler.d.ts.map +1 -1
- package/dist/src/browser/profiler.js +19 -22
- package/dist/src/browser/profiler.js.map +1 -1
- package/dist/src/browser/transport.d.ts +62 -0
- package/dist/src/browser/transport.d.ts.map +1 -0
- package/dist/src/browser/transport.js +65 -0
- package/dist/src/browser/transport.js.map +1 -0
- package/dist/src/cli/commander-adapter.d.ts +16 -0
- package/dist/src/cli/commander-adapter.d.ts.map +1 -0
- package/dist/src/cli/commander-adapter.js +68 -0
- package/dist/src/cli/commander-adapter.js.map +1 -0
- package/dist/src/cli/commands-context.d.ts +14 -0
- package/dist/src/cli/commands-context.d.ts.map +1 -0
- package/dist/src/cli/commands-context.js +351 -0
- package/dist/src/cli/commands-context.js.map +1 -0
- package/dist/src/cli/commands-data.d.ts +12 -0
- package/dist/src/cli/commands-data.d.ts.map +1 -0
- package/dist/src/cli/commands-data.js +246 -0
- package/dist/src/cli/commands-data.js.map +1 -0
- package/dist/src/cli/commands-element.d.ts +15 -0
- package/dist/src/cli/commands-element.d.ts.map +1 -0
- package/dist/src/cli/commands-element.js +262 -0
- package/dist/src/cli/commands-element.js.map +1 -0
- package/dist/src/cli/commands-files.d.ts +9 -0
- package/dist/src/cli/commands-files.d.ts.map +1 -0
- package/dist/src/cli/commands-files.js +207 -0
- package/dist/src/cli/commands-files.js.map +1 -0
- package/dist/src/cli/commands-input.d.ts +26 -0
- package/dist/src/cli/commands-input.d.ts.map +1 -0
- package/dist/src/cli/commands-input.js +399 -0
- package/dist/src/cli/commands-input.js.map +1 -0
- package/dist/src/cli/commands-navigate.d.ts +12 -0
- package/dist/src/cli/commands-navigate.d.ts.map +1 -0
- package/dist/src/cli/commands-navigate.js +389 -0
- package/dist/src/cli/commands-navigate.js.map +1 -0
- package/dist/src/cli/commands-page.d.ts +25 -0
- package/dist/src/cli/commands-page.d.ts.map +1 -0
- package/dist/src/cli/commands-page.js +377 -0
- package/dist/src/cli/commands-page.js.map +1 -0
- package/dist/src/cli/commands-report.d.ts +8 -0
- package/dist/src/cli/commands-report.d.ts.map +1 -0
- package/dist/src/cli/commands-report.js +256 -0
- package/dist/src/cli/commands-report.js.map +1 -0
- package/dist/src/cli/commands-script.d.ts +43 -0
- package/dist/src/cli/commands-script.d.ts.map +1 -0
- package/dist/src/cli/commands-script.js +168 -0
- package/dist/src/cli/commands-script.js.map +1 -0
- package/dist/src/cli/commands-session.d.ts +15 -0
- package/dist/src/cli/commands-session.d.ts.map +1 -0
- package/dist/src/cli/commands-session.js +357 -0
- package/dist/src/cli/commands-session.js.map +1 -0
- package/dist/src/cli/commands-trace.d.ts +12 -0
- package/dist/src/cli/commands-trace.d.ts.map +1 -0
- package/dist/src/cli/commands-trace.js +347 -0
- package/dist/src/cli/commands-trace.js.map +1 -0
- package/dist/src/cli/commands.d.ts +22 -42
- package/dist/src/cli/commands.d.ts.map +1 -1
- package/dist/src/cli/commands.js +48 -3400
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/session.d.ts +47 -0
- package/dist/src/cli/session.d.ts.map +1 -0
- package/dist/src/cli/session.js +343 -0
- package/dist/src/cli/session.js.map +1 -0
- package/dist/src/report/a11y.d.ts +52 -0
- package/dist/src/report/a11y.d.ts.map +1 -0
- package/dist/src/report/a11y.js +231 -0
- package/dist/src/report/a11y.js.map +1 -0
- package/dist/src/report/analyze.d.ts +20 -0
- package/dist/src/report/analyze.d.ts.map +1 -0
- package/dist/src/report/analyze.js +136 -0
- package/dist/src/report/analyze.js.map +1 -0
- package/dist/src/report/budget.d.ts +27 -0
- package/dist/src/report/budget.d.ts.map +1 -0
- package/dist/src/report/budget.js +143 -0
- package/dist/src/report/budget.js.map +1 -0
- package/dist/src/report/collect-a11y.d.ts +25 -0
- package/dist/src/report/collect-a11y.d.ts.map +1 -0
- package/dist/src/report/collect-a11y.js +154 -0
- package/dist/src/report/collect-a11y.js.map +1 -0
- package/dist/src/report/collect.d.ts +45 -0
- package/dist/src/report/collect.d.ts.map +1 -0
- package/dist/src/report/collect.js +275 -0
- package/dist/src/report/collect.js.map +1 -0
- package/dist/src/report/evidence.d.ts +80 -0
- package/dist/src/report/evidence.d.ts.map +1 -0
- package/dist/src/report/evidence.js +279 -0
- package/dist/src/report/evidence.js.map +1 -0
- package/dist/src/report/flake.d.ts +88 -0
- package/dist/src/report/flake.d.ts.map +1 -0
- package/dist/src/report/flake.js +232 -0
- package/dist/src/report/flake.js.map +1 -0
- package/dist/src/report/history.d.ts +85 -0
- package/dist/src/report/history.d.ts.map +1 -0
- package/dist/src/report/history.js +176 -0
- package/dist/src/report/history.js.map +1 -0
- package/dist/src/report/index.d.ts +102 -0
- package/dist/src/report/index.d.ts.map +1 -0
- package/dist/src/report/index.js +216 -0
- package/dist/src/report/index.js.map +1 -0
- package/dist/src/report/pixel-diff.d.ts +30 -0
- package/dist/src/report/pixel-diff.d.ts.map +1 -0
- package/dist/src/report/pixel-diff.js +175 -0
- package/dist/src/report/pixel-diff.js.map +1 -0
- package/dist/src/report/png.d.ts +28 -0
- package/dist/src/report/png.d.ts.map +1 -0
- package/dist/src/report/png.js +259 -0
- package/dist/src/report/png.js.map +1 -0
- package/dist/src/report/render-evidence.d.ts +16 -0
- package/dist/src/report/render-evidence.d.ts.map +1 -0
- package/dist/src/report/render-evidence.js +174 -0
- package/dist/src/report/render-evidence.js.map +1 -0
- package/dist/src/report/render-trend.d.ts +16 -0
- package/dist/src/report/render-trend.d.ts.map +1 -0
- package/dist/src/report/render-trend.js +194 -0
- package/dist/src/report/render-trend.js.map +1 -0
- package/dist/src/report/render.d.ts +21 -0
- package/dist/src/report/render.d.ts.map +1 -0
- package/dist/src/report/render.js +286 -0
- package/dist/src/report/render.js.map +1 -0
- package/dist/src/report/run-diff.d.ts +18 -0
- package/dist/src/report/run-diff.d.ts.map +1 -0
- package/dist/src/report/run-diff.js +51 -0
- package/dist/src/report/run-diff.js.map +1 -0
- package/dist/src/report/structure.d.ts +41 -0
- package/dist/src/report/structure.d.ts.map +1 -0
- package/dist/src/report/structure.js +210 -0
- package/dist/src/report/structure.js.map +1 -0
- package/dist/src/report/trend.d.ts +26 -0
- package/dist/src/report/trend.d.ts.map +1 -0
- package/dist/src/report/trend.js +164 -0
- package/dist/src/report/trend.js.map +1 -0
- package/dist/src/report/types.d.ts +320 -0
- package/dist/src/report/types.d.ts.map +1 -0
- package/dist/src/report/types.js +11 -0
- package/dist/src/report/types.js.map +1 -0
- package/dist/src/report/util.d.ts +17 -0
- package/dist/src/report/util.d.ts.map +1 -0
- package/dist/src/report/util.js +45 -0
- package/dist/src/report/util.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/bridge-instruments.test.ts +180 -0
- package/src/__tests__/bridge-transport.test.ts +365 -0
- package/src/__tests__/cdp-transport.test.ts +198 -0
- package/src/__tests__/profiler-heap.test.ts +191 -0
- package/src/__tests__/report-a11y.test.ts +180 -0
- package/src/__tests__/report-analyze.test.ts +202 -0
- package/src/__tests__/report-budget.test.ts +92 -0
- package/src/__tests__/report-diff.test.ts +272 -0
- package/src/__tests__/report-evidence.test.ts +276 -0
- package/src/__tests__/report-flake.test.ts +214 -0
- package/src/__tests__/report-history.test.ts +293 -0
- package/src/__tests__/report-output.test.ts +210 -0
- package/src/__tests__/report-png.test.ts +95 -0
- package/src/__tests__/report-render-extras.test.ts +315 -0
- package/src/__tests__/report-render.test.ts +175 -0
- package/src/__tests__/report-run.test.ts +264 -0
- package/src/browser/bridge.ts +354 -0
- package/src/browser/cdp.ts +57 -52
- package/src/browser/index.ts +3 -0
- package/src/browser/profiler.ts +28 -21
- package/src/browser/transport.ts +111 -0
- package/src/cli/commander-adapter.ts +65 -0
- package/src/cli/commands-context.ts +367 -0
- package/src/cli/commands-data.ts +248 -0
- package/src/cli/commands-element.ts +305 -0
- package/src/cli/commands-files.ts +248 -0
- package/src/cli/commands-input.ts +434 -0
- package/src/cli/commands-navigate.ts +442 -0
- package/src/cli/commands-page.ts +420 -0
- package/src/cli/commands-report.ts +275 -0
- package/src/cli/commands-script.ts +183 -0
- package/src/cli/commands-session.ts +376 -0
- package/src/cli/commands-trace.ts +349 -0
- package/src/cli/commands.ts +101 -3677
- package/src/cli/session.ts +412 -0
- package/src/report/a11y.ts +242 -0
- package/src/report/analyze.ts +181 -0
- package/src/report/budget.ts +155 -0
- package/src/report/collect-a11y.ts +200 -0
- package/src/report/collect.ts +408 -0
- package/src/report/evidence.ts +379 -0
- package/src/report/flake.ts +303 -0
- package/src/report/history.ts +219 -0
- package/src/report/index.ts +307 -0
- package/src/report/pixel-diff.ts +197 -0
- package/src/report/png.ts +290 -0
- package/src/report/render-evidence.ts +194 -0
- package/src/report/render-trend.ts +225 -0
- package/src/report/render.ts +314 -0
- package/src/report/run-diff.ts +63 -0
- package/src/report/structure.ts +212 -0
- package/src/report/trend.ts +189 -0
- package/src/report/types.ts +356 -0
- package/src/report/util.ts +48 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Wave 2 report sections: trend (RIG-13), run diff (RIG-10), evidence
|
|
3
|
+
* timeline (RIG-11) and flake analysis (RIG-14).
|
|
4
|
+
*
|
|
5
|
+
* The report is a single self-contained file that gets mailed around and fed
|
|
6
|
+
* to the document pipeline, so two properties matter beyond "the section is
|
|
7
|
+
* there": everything interpolated is escaped, and the JSON sibling carries
|
|
8
|
+
* the measurements without the megabytes of base64.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { buildReport } from '../report/analyze.js';
|
|
12
|
+
import { DEFAULT_BUDGET } from '../report/budget.js';
|
|
13
|
+
import { analyzeFlake } from '../report/flake.js';
|
|
14
|
+
import { toHistoryRun } from '../report/history.js';
|
|
15
|
+
import { toJsonReport } from '../report/index.js';
|
|
16
|
+
import { encodePng, pngToDataUrl } from '../report/png.js';
|
|
17
|
+
import { renderHtml } from '../report/render.js';
|
|
18
|
+
import { buildTrend } from '../report/trend.js';
|
|
19
|
+
import type { CaptureData, Evidence, Report, RunDiff } from '../report/types.js';
|
|
20
|
+
|
|
21
|
+
function capture(partial: Partial<CaptureData> = {}): CaptureData {
|
|
22
|
+
return {
|
|
23
|
+
url: 'https://shop.test/checkout',
|
|
24
|
+
finalUrl: 'https://shop.test/checkout',
|
|
25
|
+
title: 'Checkout',
|
|
26
|
+
capturedAt: '2026-09-21T10:00:00.000Z',
|
|
27
|
+
durationMs: 1500,
|
|
28
|
+
console: [],
|
|
29
|
+
pageErrors: [],
|
|
30
|
+
requests: [],
|
|
31
|
+
vitals: { lcp: 2400, cls: 0.02, inp: 60 },
|
|
32
|
+
a11y: [],
|
|
33
|
+
screenshots: [],
|
|
34
|
+
notes: [],
|
|
35
|
+
...partial,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const report = (partial: Partial<CaptureData> = {}): Report =>
|
|
40
|
+
buildReport(capture(partial), DEFAULT_BUDGET);
|
|
41
|
+
|
|
42
|
+
function tinyPngDataUrl(shade: number) {
|
|
43
|
+
const data = new Uint8Array(4 * 4 * 4);
|
|
44
|
+
for (let i = 0; i < 16; i++) data.set([shade, shade, shade, 255], i * 4);
|
|
45
|
+
return pngToDataUrl(encodePng({ width: 4, height: 4, data }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('trend section', () => {
|
|
49
|
+
it('says so plainly on a first run instead of drawing an empty chart', () => {
|
|
50
|
+
const r = report();
|
|
51
|
+
r.trend = buildTrend(r, []);
|
|
52
|
+
const html = renderHtml(r);
|
|
53
|
+
expect(html).toContain('First recorded run for this URL');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('leads with the creep headline and draws a sparkline', () => {
|
|
57
|
+
const history = Array.from({ length: 5 }, (_, i) =>
|
|
58
|
+
toHistoryRun(
|
|
59
|
+
report({ capturedAt: `2026-09-1${i}T10:00:00.000Z`, vitals: { lcp: 2000 } }),
|
|
60
|
+
`id${i}`,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
const r = report({ vitals: { lcp: 2400 } });
|
|
64
|
+
r.trend = buildTrend(r, history);
|
|
65
|
+
const html = renderHtml(r);
|
|
66
|
+
expect(html).toContain('Trend — this run against the previous 5');
|
|
67
|
+
expect(html).toContain('LCP has crept up 400ms');
|
|
68
|
+
expect(html).toContain('<svg class="spark"');
|
|
69
|
+
expect(html).toContain('verdictstrip');
|
|
70
|
+
// Lower-is-better is stated, or a positive delta reads as good news.
|
|
71
|
+
expect(html).toContain('lower-is-better');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('omits a metric no run ever measured', () => {
|
|
75
|
+
const history = [toHistoryRun(report({ vitals: { lcp: 2000 } }), 'id0')];
|
|
76
|
+
const r = report({ vitals: { lcp: 2400 } });
|
|
77
|
+
r.trend = buildTrend(r, history);
|
|
78
|
+
const html = renderHtml(r);
|
|
79
|
+
// Scoped to the trend table: the Wave 1 vitals table lists every metric
|
|
80
|
+
// whether or not it was measured, which is correct for that section.
|
|
81
|
+
const section = html.slice(html.indexOf('<h2>Trend'), html.indexOf('<h2>Errors'));
|
|
82
|
+
expect(section).toContain('<b>LCP</b>');
|
|
83
|
+
expect(section).not.toContain('<b>TTFB</b>');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('diff section', () => {
|
|
88
|
+
const diff: RunDiff = {
|
|
89
|
+
previousRunId: '20260920T100000Z-abcdabcd',
|
|
90
|
+
previousCapturedAt: '2026-09-20T10:00:00.000Z',
|
|
91
|
+
notes: [],
|
|
92
|
+
structure: {
|
|
93
|
+
gained: [{ path: '/main[1]/button[1]', role: 'button', name: 'Cancel', depth: 1 }],
|
|
94
|
+
lost: [{ path: '/main[1]/link[1]', role: 'link', name: 'Back', depth: 1 }],
|
|
95
|
+
renamed: [{ path: '/main[1]/button[2]', role: 'button', from: 'Pay now', to: 'Place order' }],
|
|
96
|
+
moved: [],
|
|
97
|
+
unchanged: 42,
|
|
98
|
+
changed: 3,
|
|
99
|
+
},
|
|
100
|
+
pixels: {
|
|
101
|
+
comparable: true,
|
|
102
|
+
changedPixels: 1234,
|
|
103
|
+
totalPixels: 100_000,
|
|
104
|
+
changedPercent: 1.234,
|
|
105
|
+
previousSize: { width: 400, height: 250 },
|
|
106
|
+
currentSize: { width: 400, height: 300 },
|
|
107
|
+
sizeChanged: true,
|
|
108
|
+
diffDataUrl: tinyPngDataUrl(120),
|
|
109
|
+
scale: 2,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
it('lists what was gained, lost and renamed', () => {
|
|
114
|
+
const r = report();
|
|
115
|
+
r.diff = diff;
|
|
116
|
+
const html = renderHtml(r);
|
|
117
|
+
expect(html).toContain('Changes since the previous run');
|
|
118
|
+
expect(html).toContain('2026-09-20T10:00:00.000Z');
|
|
119
|
+
expect(html).toContain('Cancel');
|
|
120
|
+
expect(html).toContain('Back');
|
|
121
|
+
expect(html).toContain('Place order');
|
|
122
|
+
expect(html).toContain('42</b> unchanged');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('reports the changed percentage, the size change and the highlight image', () => {
|
|
126
|
+
const r = report();
|
|
127
|
+
r.diff = diff;
|
|
128
|
+
const html = renderHtml(r);
|
|
129
|
+
expect(html).toContain('1.23%</b> of pixels changed');
|
|
130
|
+
expect(html).toContain('400×250');
|
|
131
|
+
expect(html).toContain('400×300');
|
|
132
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
133
|
+
expect(html).toContain('1/2 scale');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('says the tree is identical rather than showing an empty table', () => {
|
|
137
|
+
const r = report();
|
|
138
|
+
r.diff = {
|
|
139
|
+
...diff,
|
|
140
|
+
structure: { gained: [], lost: [], renamed: [], moved: [], unchanged: 40, changed: 0 },
|
|
141
|
+
pixels: undefined,
|
|
142
|
+
};
|
|
143
|
+
expect(renderHtml(r)).toContain('identical to the previous run');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('escapes an accessible name that contains markup', () => {
|
|
147
|
+
const r = report();
|
|
148
|
+
r.diff = {
|
|
149
|
+
...diff,
|
|
150
|
+
structure: {
|
|
151
|
+
gained: [
|
|
152
|
+
{
|
|
153
|
+
path: '/main[1]/button[1]',
|
|
154
|
+
role: 'button',
|
|
155
|
+
name: '<img src=x onerror="alert(1)">',
|
|
156
|
+
depth: 1,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
lost: [],
|
|
160
|
+
renamed: [],
|
|
161
|
+
moved: [],
|
|
162
|
+
unchanged: 0,
|
|
163
|
+
changed: 1,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const html = renderHtml(r);
|
|
167
|
+
expect(html).not.toContain('<img src=x');
|
|
168
|
+
expect(html).toContain('<img src=x onerror="alert(1)">');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('evidence section', () => {
|
|
173
|
+
const evidence: Evidence = {
|
|
174
|
+
reason: 'budget-failure',
|
|
175
|
+
focusOffsetMs: 1200,
|
|
176
|
+
frames: [
|
|
177
|
+
{ offsetMs: 1100, dataUrl: 'data:image/jpeg;base64,AAAA', bytes: 4 },
|
|
178
|
+
{ offsetMs: 1200, dataUrl: 'data:image/jpeg;base64,BBBB', bytes: 4 },
|
|
179
|
+
],
|
|
180
|
+
timeline: [
|
|
181
|
+
{ offsetMs: 1100, kind: 'frame', label: 'frame', severity: 'info', frameIndex: 0 },
|
|
182
|
+
{
|
|
183
|
+
offsetMs: 1200,
|
|
184
|
+
kind: 'console',
|
|
185
|
+
label: 'console.error',
|
|
186
|
+
detail: 'Cannot read properties of undefined',
|
|
187
|
+
severity: 'error',
|
|
188
|
+
},
|
|
189
|
+
{ offsetMs: 1200, kind: 'marker', label: 'first failure', severity: 'error' },
|
|
190
|
+
],
|
|
191
|
+
droppedFrames: 18,
|
|
192
|
+
totalBytes: 8,
|
|
193
|
+
notes: ['18 recorded frame(s) not attached — the report keeps the 2 nearest the failure.'],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
it('renders a filmstrip and a time-ordered timeline', () => {
|
|
197
|
+
const r = report();
|
|
198
|
+
r.evidence = evidence;
|
|
199
|
+
const html = renderHtml(r);
|
|
200
|
+
expect(html).toContain('<h2>Evidence</h2>');
|
|
201
|
+
expect(html).toContain('filmstrip');
|
|
202
|
+
expect(html).toContain('data:image/jpeg;base64,BBBB');
|
|
203
|
+
expect(html).toContain('Cannot read properties of undefined');
|
|
204
|
+
expect(html).toContain('first failure');
|
|
205
|
+
expect(html).toContain('+1.20s');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('marks the frame nearest the failure', () => {
|
|
209
|
+
const r = report();
|
|
210
|
+
r.evidence = evidence;
|
|
211
|
+
const html = renderHtml(r);
|
|
212
|
+
expect(html).toContain('at failure');
|
|
213
|
+
expect(html).toContain('<figure class="focus">');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('says why it recorded and surfaces its own bounds', () => {
|
|
217
|
+
const r = report();
|
|
218
|
+
r.evidence = evidence;
|
|
219
|
+
const html = renderHtml(r);
|
|
220
|
+
expect(html).toContain('breached its budget');
|
|
221
|
+
expect(html).toContain('not attached');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('is absent entirely when nothing was recorded', () => {
|
|
225
|
+
expect(renderHtml(report())).not.toContain('<h2>Evidence</h2>');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('flake section', () => {
|
|
230
|
+
const err = (text: string) => ({ type: 'error', text, timestamp: 0 });
|
|
231
|
+
|
|
232
|
+
it('shows a FLAKY banner and the per-check rates', () => {
|
|
233
|
+
const flake = analyzeFlake([report(), report({ console: [err('boom')] }), report()]);
|
|
234
|
+
const html = renderHtml(report(), { flake });
|
|
235
|
+
expect(html).toContain('Repeatability — 3 runs');
|
|
236
|
+
expect(html).toContain('>FLAKY<');
|
|
237
|
+
expect(html).toContain('2 of 3 passed');
|
|
238
|
+
expect(html).toContain('Confidence: high.');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('states what a clean window does not prove', () => {
|
|
242
|
+
const flake = analyzeFlake([report(), report()]);
|
|
243
|
+
const html = renderHtml(report(), { flake });
|
|
244
|
+
expect(html).toContain('>PASS<');
|
|
245
|
+
expect(html).toContain('95% confidence');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('lists an intermittent error with the runs it appeared in', () => {
|
|
249
|
+
const flake = analyzeFlake([report(), report({ console: [err('flaky fetch')] }), report()]);
|
|
250
|
+
const html = renderHtml(report(), { flake });
|
|
251
|
+
expect(html).toContain('flaky fetch');
|
|
252
|
+
expect(html).toContain('1 of 3');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('is absent for an ordinary single run', () => {
|
|
256
|
+
expect(renderHtml(report())).not.toContain('Repeatability');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('toJsonReport', () => {
|
|
261
|
+
it('keeps the pixel measurements but drops the highlight image', () => {
|
|
262
|
+
const r = report();
|
|
263
|
+
r.diff = {
|
|
264
|
+
previousRunId: 'x',
|
|
265
|
+
previousCapturedAt: '2026-09-20T10:00:00.000Z',
|
|
266
|
+
notes: [],
|
|
267
|
+
pixels: {
|
|
268
|
+
comparable: true,
|
|
269
|
+
changedPixels: 10,
|
|
270
|
+
totalPixels: 100,
|
|
271
|
+
changedPercent: 10,
|
|
272
|
+
previousSize: { width: 4, height: 4 },
|
|
273
|
+
currentSize: { width: 4, height: 4 },
|
|
274
|
+
sizeChanged: false,
|
|
275
|
+
diffDataUrl: tinyPngDataUrl(10),
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
const json = toJsonReport(r) as { diff: { pixels: Record<string, unknown> } };
|
|
279
|
+
expect(json.diff.pixels.changedPercent).toBe(10);
|
|
280
|
+
expect(json.diff.pixels.diffDataUrl).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('keeps frame offsets but drops the frame images', () => {
|
|
284
|
+
const r = report();
|
|
285
|
+
r.evidence = {
|
|
286
|
+
reason: 'requested',
|
|
287
|
+
focusOffsetMs: null,
|
|
288
|
+
frames: [{ offsetMs: 500, dataUrl: 'data:image/jpeg;base64,AAAA', bytes: 4 }],
|
|
289
|
+
timeline: [],
|
|
290
|
+
droppedFrames: 0,
|
|
291
|
+
totalBytes: 4,
|
|
292
|
+
notes: [],
|
|
293
|
+
};
|
|
294
|
+
const json = toJsonReport(r) as { evidence: { frames: Array<Record<string, unknown>> } };
|
|
295
|
+
expect(json.evidence.frames).toEqual([{ offsetMs: 500, bytes: 4 }]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('never serializes the raw recording buffer', () => {
|
|
299
|
+
const r = report();
|
|
300
|
+
r.recording = {
|
|
301
|
+
startedAtMs: 1,
|
|
302
|
+
frames: [{ offsetMs: 0, data: 'x', bytes: 1 }],
|
|
303
|
+
droppedFrames: 0,
|
|
304
|
+
requested: true,
|
|
305
|
+
};
|
|
306
|
+
expect(JSON.stringify(toJsonReport(r))).not.toContain('"recording"');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('carries the trend through unchanged', () => {
|
|
310
|
+
const r = report();
|
|
311
|
+
r.trend = buildTrend(r, []);
|
|
312
|
+
const json = toJsonReport(r) as { trend: { runs: number } };
|
|
313
|
+
expect(json.trend.runs).toBe(0);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The renderer's contract is that the HTML is SELF-CONTAINED: it must render
|
|
3
|
+
* identically on a machine with no network, because monomind's document
|
|
4
|
+
* pipeline ingests it as an ordinary document and because an archived report
|
|
5
|
+
* has to still work years later. These tests enforce that mechanically.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { buildReport } from '../report/analyze.js';
|
|
9
|
+
import { DEFAULT_BUDGET, parseBudget } from '../report/budget.js';
|
|
10
|
+
import { escapeHtml, renderHtml } from '../report/render.js';
|
|
11
|
+
import type { CaptureData, Report } from '../report/types.js';
|
|
12
|
+
|
|
13
|
+
const PNG =
|
|
14
|
+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
15
|
+
|
|
16
|
+
function capture(partial: Partial<CaptureData> = {}): CaptureData {
|
|
17
|
+
return {
|
|
18
|
+
url: 'https://shop.test/checkout',
|
|
19
|
+
finalUrl: 'https://shop.test/checkout',
|
|
20
|
+
title: 'Checkout',
|
|
21
|
+
capturedAt: '2026-09-21T10:00:00.000Z',
|
|
22
|
+
durationMs: 3200,
|
|
23
|
+
console: [],
|
|
24
|
+
pageErrors: [],
|
|
25
|
+
requests: [],
|
|
26
|
+
vitals: {},
|
|
27
|
+
a11y: [],
|
|
28
|
+
screenshots: [],
|
|
29
|
+
notes: [],
|
|
30
|
+
...partial,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function report(partial: Partial<CaptureData> = {}, budget = DEFAULT_BUDGET): Report {
|
|
35
|
+
return buildReport(capture(partial), budget);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Anything that would make the browser go to the network to render the page. */
|
|
39
|
+
function externalReferences(html: string): string[] {
|
|
40
|
+
const patterns = [
|
|
41
|
+
/<script[^>]+\ssrc\s*=/gi,
|
|
42
|
+
/<link\b/gi,
|
|
43
|
+
/@import/gi,
|
|
44
|
+
/url\(\s*['"]?https?:/gi,
|
|
45
|
+
/\s(?:src|href|poster|action|data)\s*=\s*["']https?:/gi,
|
|
46
|
+
/\s(?:src|href)\s*=\s*["']\/\//gi,
|
|
47
|
+
];
|
|
48
|
+
return patterns.flatMap((p) => html.match(p) ?? []);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('renderHtml — self-containment', () => {
|
|
52
|
+
it('references no external resource of any kind', () => {
|
|
53
|
+
const html = renderHtml(
|
|
54
|
+
report({
|
|
55
|
+
screenshots: [{ label: 'page', width: 1280, height: 3000, dataUrl: PNG }],
|
|
56
|
+
requests: [{ url: 'https://cdn.test/app.js', method: 'GET', status: 404, failed: true }],
|
|
57
|
+
console: [{ type: 'error', text: 'boom', url: 'https://cdn.test/app.js', timestamp: 0 }],
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
expect(externalReferences(html)).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('inlines its CSS in a style element and inlines screenshots as data URIs', () => {
|
|
64
|
+
const html = renderHtml(
|
|
65
|
+
report({ screenshots: [{ label: 'page', width: 800, height: 600, dataUrl: PNG }] }),
|
|
66
|
+
);
|
|
67
|
+
expect(html).toContain('<style>');
|
|
68
|
+
expect(html).toContain('src="data:image/png;base64,');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('themes for both light and dark without JavaScript', () => {
|
|
72
|
+
const html = renderHtml(report());
|
|
73
|
+
expect(html).toContain('@media (prefers-color-scheme: dark)');
|
|
74
|
+
expect(html).not.toMatch(/<script(?![^>]*type="application\/json")/i);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('renderHtml — content', () => {
|
|
79
|
+
it('leads with a PASS verdict for a clean page', () => {
|
|
80
|
+
const html = renderHtml(report({ vitals: { lcp: 800, cls: 0.01, inp: 20 } }));
|
|
81
|
+
const verdictAt = html.indexOf('PASS');
|
|
82
|
+
expect(verdictAt).toBeGreaterThan(-1);
|
|
83
|
+
expect(verdictAt).toBeLessThan(html.indexOf('Failed requests'));
|
|
84
|
+
expect(html).toContain('All budgets met.');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('orders the sections verdict, errors, failed requests, vitals, a11y, screenshots', () => {
|
|
88
|
+
const html = renderHtml(report());
|
|
89
|
+
const order = [
|
|
90
|
+
'PASS',
|
|
91
|
+
'Errors',
|
|
92
|
+
'Failed requests',
|
|
93
|
+
'Web vitals',
|
|
94
|
+
'Accessibility',
|
|
95
|
+
'Screenshots',
|
|
96
|
+
];
|
|
97
|
+
const positions = order.map((s) => html.indexOf(s));
|
|
98
|
+
expect(positions.every((p) => p > -1)).toBe(true);
|
|
99
|
+
expect([...positions].sort((a, b) => a - b)).toEqual(positions);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('shows each budget failure with its expected and actual values', () => {
|
|
103
|
+
const html = renderHtml(
|
|
104
|
+
report({ vitals: { lcp: 4100, cls: 0, inp: 0 } }, parseBudget({ lcp: 2500 })),
|
|
105
|
+
);
|
|
106
|
+
expect(html).toContain('FAIL');
|
|
107
|
+
expect(html).toContain('LCP (Largest Contentful Paint)');
|
|
108
|
+
expect(html).toContain('<= 2500ms');
|
|
109
|
+
expect(html).toContain('4100ms');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('lists failed requests and omits the successful ones', () => {
|
|
113
|
+
const html = renderHtml(
|
|
114
|
+
report({
|
|
115
|
+
requests: [
|
|
116
|
+
{ url: 'https://shop.test/api/cart', method: 'POST', status: 500, failed: true },
|
|
117
|
+
{ url: 'https://shop.test/ok', method: 'GET', status: 200, failed: false },
|
|
118
|
+
],
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
expect(html).toContain('https://shop.test/api/cart');
|
|
122
|
+
expect(html).not.toContain('https://shop.test/ok');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('says a metric was not measured rather than showing a fake zero', () => {
|
|
126
|
+
const html = renderHtml(report({ vitals: { cls: 0.05 } }));
|
|
127
|
+
expect(html).toContain('not measured');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders a11y findings with role, name state and locator', () => {
|
|
131
|
+
const html = renderHtml(
|
|
132
|
+
report({
|
|
133
|
+
a11y: [
|
|
134
|
+
{
|
|
135
|
+
rule: 'unlabelled-control',
|
|
136
|
+
impact: 'error',
|
|
137
|
+
role: 'button',
|
|
138
|
+
name: null,
|
|
139
|
+
locator: 'form > button:nth-of-type(2)',
|
|
140
|
+
detail: 'button has no accessible name',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
expect(html).toContain('unlabelled-control');
|
|
146
|
+
expect(html).toContain('form > button:nth-of-type(2)');
|
|
147
|
+
expect(html).toContain('(no accessible name)');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('states plainly that contrast was not evaluated', () => {
|
|
151
|
+
expect(renderHtml(report())).toMatch(/[Cc]olour contrast is not evaluated/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('surfaces degraded-run notes', () => {
|
|
155
|
+
const html = renderHtml(report({ notes: ['Page never went network-idle within 20000ms'] }));
|
|
156
|
+
expect(html).toContain('Run notes');
|
|
157
|
+
expect(html).toContain('never went network-idle');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('escapeHtml', () => {
|
|
162
|
+
it('neutralizes markup from page-controlled strings', () => {
|
|
163
|
+
expect(escapeHtml('<img src=x onerror="alert(1)">')).toBe(
|
|
164
|
+
'<img src=x onerror="alert(1)">',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('keeps an injected error message from breaking out of the document', () => {
|
|
169
|
+
const html = renderHtml(
|
|
170
|
+
report({ pageErrors: [{ text: '</table><script>alert(1)</script>', timestamp: 0 }] }),
|
|
171
|
+
);
|
|
172
|
+
expect(html).not.toContain('<script>alert(1)</script>');
|
|
173
|
+
expect(html).toContain('<script>');
|
|
174
|
+
});
|
|
175
|
+
});
|