@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,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drives the browser and gathers the raw material for a report.
|
|
3
|
+
*
|
|
4
|
+
* Resilience is the design constraint: a page with no vitals, a site that
|
|
5
|
+
* never goes network-idle, a CSP that blocks our evaluate — each must
|
|
6
|
+
* degrade into a note in the report, never a hang and never a crash. Every
|
|
7
|
+
* collector is individually timed out and individually try/caught, and this
|
|
8
|
+
* function always returns a CaptureData.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { evaluateJs } from '../browser/actions.js';
|
|
12
|
+
import { getCurrentTitle, getCurrentUrl, openUrl, waitForLoad } from '../browser/browser.js';
|
|
13
|
+
import type { CdpClient } from '../browser/cdp.js';
|
|
14
|
+
import {
|
|
15
|
+
clearConsoleMessages,
|
|
16
|
+
clearPageErrors,
|
|
17
|
+
enableConsoleCapture,
|
|
18
|
+
getConsoleMessages,
|
|
19
|
+
getPageErrors,
|
|
20
|
+
setupConsoleCapture,
|
|
21
|
+
} from '../browser/console-log.js';
|
|
22
|
+
import { emulateDevice, listDevices } from '../browser/emulation.js';
|
|
23
|
+
import {
|
|
24
|
+
clearCapturedRequests,
|
|
25
|
+
getCapturedRequests,
|
|
26
|
+
startRequestCapture,
|
|
27
|
+
} from '../browser/network.js';
|
|
28
|
+
import { captureScreenshot } from '../browser/screenshot.js';
|
|
29
|
+
import { collectVitals } from '../browser/vitals.js';
|
|
30
|
+
import { waitFor } from '../browser/wait.js';
|
|
31
|
+
import { collectA11y } from './collect-a11y.js';
|
|
32
|
+
import type { EvidenceRecorder, EvidenceRecorderOptions } from './evidence.js';
|
|
33
|
+
import { startEvidenceRecorder } from './evidence.js';
|
|
34
|
+
import type { CaptureData, RequestEntry, Screenshot, VitalsData } from './types.js';
|
|
35
|
+
import { message, STEP_TIMEOUT_MS, sleep, withTimeout } from './util.js';
|
|
36
|
+
|
|
37
|
+
export interface CollectOptions {
|
|
38
|
+
url: string;
|
|
39
|
+
/** A CSS selector to wait for, or a number of milliseconds. */
|
|
40
|
+
wait?: string | number;
|
|
41
|
+
/** Device names from `listDevices()` to add to the screenshot matrix. */
|
|
42
|
+
devices?: string[];
|
|
43
|
+
vitalsWaitMs?: number;
|
|
44
|
+
loadTimeoutMs?: number;
|
|
45
|
+
screenshotDir?: string;
|
|
46
|
+
fullPage?: boolean;
|
|
47
|
+
/** Record screencast frames for the evidence timeline (RIG-11). */
|
|
48
|
+
record?: boolean;
|
|
49
|
+
recordOptions?: EvidenceRecorderOptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEFAULT_VITALS_WAIT_MS = 2500;
|
|
53
|
+
const DEFAULT_LOAD_TIMEOUT_MS = 20_000;
|
|
54
|
+
/** Cap on inlined screenshot bytes so the single HTML file stays openable. */
|
|
55
|
+
const MAX_INLINE_BYTES = 12 * 1024 * 1024;
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Network failures
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
interface FailureRecord {
|
|
62
|
+
errorText?: string;
|
|
63
|
+
blockedReason?: string;
|
|
64
|
+
canceled?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CDP reports request timings on a monotonic clock with an arbitrary epoch,
|
|
69
|
+
* which cannot be compared with the `Date.now()` stamps on console entries.
|
|
70
|
+
* Sampling both clocks on the first request we see gives an offset that puts
|
|
71
|
+
* every request on the same timeline as everything else (RIG-11).
|
|
72
|
+
*/
|
|
73
|
+
function trackClockOffset(
|
|
74
|
+
client: CdpClient,
|
|
75
|
+
sessionId: string,
|
|
76
|
+
): { offset: () => number | undefined; stop: () => void } {
|
|
77
|
+
let offset: number | undefined;
|
|
78
|
+
const off = client.on('Network.requestWillBeSent', (params, sid) => {
|
|
79
|
+
if (sid !== sessionId || offset !== undefined) return;
|
|
80
|
+
const p = params as { timestamp?: number };
|
|
81
|
+
if (typeof p.timestamp !== 'number') return;
|
|
82
|
+
offset = Date.now() - p.timestamp * 1000;
|
|
83
|
+
});
|
|
84
|
+
return { offset: () => offset, stop: off };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* `startRequestCapture` records timings but drops `Network.loadingFailed`'s
|
|
89
|
+
* errorText, so a DNS failure and a still-in-flight request look identical in
|
|
90
|
+
* its output. Listen alongside it rather than editing it.
|
|
91
|
+
*/
|
|
92
|
+
function trackLoadingFailures(
|
|
93
|
+
client: CdpClient,
|
|
94
|
+
sessionId: string,
|
|
95
|
+
): { failures: Map<string, FailureRecord>; stop: () => void } {
|
|
96
|
+
const failures = new Map<string, FailureRecord>();
|
|
97
|
+
const off = client.on('Network.loadingFailed', (params, sid) => {
|
|
98
|
+
if (sid !== sessionId) return;
|
|
99
|
+
const p = params as {
|
|
100
|
+
requestId?: string;
|
|
101
|
+
errorText?: string;
|
|
102
|
+
blockedReason?: string;
|
|
103
|
+
canceled?: boolean;
|
|
104
|
+
};
|
|
105
|
+
if (!p.requestId) return;
|
|
106
|
+
failures.set(p.requestId, {
|
|
107
|
+
errorText: p.errorText,
|
|
108
|
+
blockedReason: p.blockedReason,
|
|
109
|
+
canceled: p.canceled,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
return { failures, stop: off };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CapturedRequestLike {
|
|
116
|
+
id: string;
|
|
117
|
+
url: string;
|
|
118
|
+
method: string;
|
|
119
|
+
status?: number;
|
|
120
|
+
mimeType?: string;
|
|
121
|
+
startTime: number;
|
|
122
|
+
endTime?: number;
|
|
123
|
+
encodedSize?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function toRequestEntries(
|
|
127
|
+
captured: CapturedRequestLike[],
|
|
128
|
+
failures: Map<string, FailureRecord>,
|
|
129
|
+
clockOffsetMs?: number,
|
|
130
|
+
): RequestEntry[] {
|
|
131
|
+
return captured.map((r) => {
|
|
132
|
+
const failure = failures.get(r.id);
|
|
133
|
+
const httpError = r.status !== undefined && r.status >= 400;
|
|
134
|
+
// A cancelled request is usually the page itself aborting a fetch on
|
|
135
|
+
// navigation — noise, not a defect. A blockedReason (CSP, mixed content,
|
|
136
|
+
// an ad blocker) is a real failure even though CDP also marks it cancelled.
|
|
137
|
+
const hardFailure = failure !== undefined && (!failure.canceled || !!failure.blockedReason);
|
|
138
|
+
const errorText = failure?.blockedReason
|
|
139
|
+
? `blocked: ${failure.blockedReason}`
|
|
140
|
+
: (failure?.errorText ?? undefined);
|
|
141
|
+
return {
|
|
142
|
+
url: r.url,
|
|
143
|
+
method: r.method,
|
|
144
|
+
startedAtMs: clockOffsetMs === undefined ? undefined : r.startTime + clockOffsetMs,
|
|
145
|
+
status: r.status,
|
|
146
|
+
mimeType: r.mimeType,
|
|
147
|
+
durationMs: r.endTime !== undefined ? Math.max(0, r.endTime - r.startTime) : undefined,
|
|
148
|
+
encodedSize: r.encodedSize,
|
|
149
|
+
failed: httpError || hardFailure,
|
|
150
|
+
errorText: hardFailure ? errorText : undefined,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Screenshots
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
async function measure(
|
|
160
|
+
client: CdpClient,
|
|
161
|
+
sessionId: string,
|
|
162
|
+
fullPage: boolean,
|
|
163
|
+
): Promise<{ width: number; height: number }> {
|
|
164
|
+
try {
|
|
165
|
+
const raw = await evaluateJs(
|
|
166
|
+
client,
|
|
167
|
+
sessionId,
|
|
168
|
+
fullPage
|
|
169
|
+
? '({width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight})'
|
|
170
|
+
: '({width: window.innerWidth, height: window.innerHeight})',
|
|
171
|
+
);
|
|
172
|
+
const size = raw as { width?: number; height?: number } | null;
|
|
173
|
+
return { width: Math.round(size?.width ?? 0), height: Math.round(size?.height ?? 0) };
|
|
174
|
+
} catch {
|
|
175
|
+
return { width: 0, height: 0 };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function takeShot(
|
|
180
|
+
client: CdpClient,
|
|
181
|
+
sessionId: string,
|
|
182
|
+
label: string,
|
|
183
|
+
fullPage: boolean,
|
|
184
|
+
dir: string | undefined,
|
|
185
|
+
notes: string[],
|
|
186
|
+
inlineBudget: { left: number },
|
|
187
|
+
): Promise<Screenshot | null> {
|
|
188
|
+
try {
|
|
189
|
+
const slug = label.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
|
|
190
|
+
const path = dir ? `${dir}/${slug}.png` : undefined;
|
|
191
|
+
const shot = await withTimeout(
|
|
192
|
+
captureScreenshot(client, sessionId, { fullPage, path }),
|
|
193
|
+
STEP_TIMEOUT_MS,
|
|
194
|
+
`screenshot (${label})`,
|
|
195
|
+
);
|
|
196
|
+
if (shot.dataUrl.length > inlineBudget.left) {
|
|
197
|
+
notes.push(
|
|
198
|
+
`Screenshot "${label}" omitted from the HTML — inlining it would blow the size budget.`,
|
|
199
|
+
);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
inlineBudget.left -= shot.dataUrl.length;
|
|
203
|
+
const size = await measure(client, sessionId, fullPage);
|
|
204
|
+
return {
|
|
205
|
+
label,
|
|
206
|
+
width: size.width,
|
|
207
|
+
height: size.height,
|
|
208
|
+
dataUrl: shot.dataUrl,
|
|
209
|
+
path: shot.path,
|
|
210
|
+
};
|
|
211
|
+
} catch (err) {
|
|
212
|
+
notes.push(`Screenshot "${label}" failed: ${message(err)}`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function captureDeviceMatrix(
|
|
218
|
+
client: CdpClient,
|
|
219
|
+
sessionId: string,
|
|
220
|
+
url: string,
|
|
221
|
+
devices: string[],
|
|
222
|
+
dir: string | undefined,
|
|
223
|
+
notes: string[],
|
|
224
|
+
inlineBudget: { left: number },
|
|
225
|
+
): Promise<Screenshot[]> {
|
|
226
|
+
const known = new Set(listDevices());
|
|
227
|
+
const shots: Screenshot[] = [];
|
|
228
|
+
for (const device of devices) {
|
|
229
|
+
if (!known.has(device)) {
|
|
230
|
+
notes.push(`Unknown device "${device}" — known devices: ${[...known].join(', ')}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await emulateDevice(client, sessionId, device);
|
|
235
|
+
// Re-navigate: responsive layout and media queries only settle on a
|
|
236
|
+
// fresh load, so resizing an already-rendered page can photograph a
|
|
237
|
+
// layout the device would never actually produce.
|
|
238
|
+
await openUrl(client, sessionId, url).catch(() => {});
|
|
239
|
+
await waitForLoad(client, sessionId, 'domcontentloaded', 5000).catch(() => {});
|
|
240
|
+
const shot = await takeShot(client, sessionId, device, false, dir, notes, inlineBudget);
|
|
241
|
+
if (shot) shots.push(shot);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
notes.push(`Device "${device}" skipped: ${message(err)}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Put the tab back the way we found it, so a later command in the same
|
|
247
|
+
// session is not silently stuck emulating an iPhone.
|
|
248
|
+
await client.send('Emulation.clearDeviceMetricsOverride', {}, sessionId).catch(() => {});
|
|
249
|
+
await client
|
|
250
|
+
.send('Emulation.setTouchEmulationEnabled', { enabled: false }, sessionId)
|
|
251
|
+
.catch(() => {});
|
|
252
|
+
await client.send('Emulation.setUserAgentOverride', { userAgent: '' }, sessionId).catch(() => {});
|
|
253
|
+
return shots;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Orchestration
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
export async function collect(
|
|
261
|
+
client: CdpClient,
|
|
262
|
+
sessionId: string,
|
|
263
|
+
options: CollectOptions,
|
|
264
|
+
): Promise<CaptureData> {
|
|
265
|
+
const notes: string[] = [];
|
|
266
|
+
const startedAt = Date.now();
|
|
267
|
+
const capturedAt = new Date().toISOString();
|
|
268
|
+
const loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_LOAD_TIMEOUT_MS;
|
|
269
|
+
const fullPage = options.fullPage ?? true;
|
|
270
|
+
|
|
271
|
+
// Capture must be armed BEFORE navigating, or the errors and requests that
|
|
272
|
+
// fire during load — the interesting ones — are already gone.
|
|
273
|
+
setupConsoleCapture(client, sessionId);
|
|
274
|
+
await enableConsoleCapture(client, sessionId).catch(() => {});
|
|
275
|
+
clearConsoleMessages(sessionId);
|
|
276
|
+
clearPageErrors(sessionId);
|
|
277
|
+
startRequestCapture(client, sessionId);
|
|
278
|
+
clearCapturedRequests(sessionId);
|
|
279
|
+
const tracker = trackLoadingFailures(client, sessionId);
|
|
280
|
+
const clock = trackClockOffset(client, sessionId);
|
|
281
|
+
|
|
282
|
+
// Armed before navigation for the same reason console capture is: the
|
|
283
|
+
// frames worth having are the ones showing the page fail to load.
|
|
284
|
+
let recorder: EvidenceRecorder | null = null;
|
|
285
|
+
if (options.record) {
|
|
286
|
+
try {
|
|
287
|
+
recorder = await startEvidenceRecorder(client, sessionId, startedAt, options.recordOptions);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
notes.push(`Screen recording unavailable: ${message(err)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const navigation = openUrl(client, sessionId, options.url)
|
|
295
|
+
.then(() => 'ok' as const)
|
|
296
|
+
.catch((err: unknown) => {
|
|
297
|
+
notes.push(`Navigation reported an error: ${message(err)}`);
|
|
298
|
+
return 'error' as const;
|
|
299
|
+
});
|
|
300
|
+
const outcome = await Promise.race([
|
|
301
|
+
navigation,
|
|
302
|
+
sleep(loadTimeoutMs).then(() => 'timeout' as const),
|
|
303
|
+
]);
|
|
304
|
+
if (outcome === 'timeout') {
|
|
305
|
+
notes.push(
|
|
306
|
+
`Page never went network-idle within ${loadTimeoutMs}ms — reporting on what had loaded by then.`,
|
|
307
|
+
);
|
|
308
|
+
await waitForLoad(client, sessionId, 'domcontentloaded', 5000).catch(() => {});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (options.wait !== undefined) {
|
|
312
|
+
const waitMs = typeof options.wait === 'number' ? options.wait : Number(options.wait);
|
|
313
|
+
try {
|
|
314
|
+
if (Number.isFinite(waitMs)) {
|
|
315
|
+
await sleep(waitMs);
|
|
316
|
+
} else {
|
|
317
|
+
await waitFor(client, sessionId, { selector: String(options.wait), timeout: 15_000 });
|
|
318
|
+
}
|
|
319
|
+
} catch (err) {
|
|
320
|
+
notes.push(`--wait not satisfied: ${message(err)}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let vitals: VitalsData = {};
|
|
325
|
+
const vitalsWaitMs = options.vitalsWaitMs ?? DEFAULT_VITALS_WAIT_MS;
|
|
326
|
+
try {
|
|
327
|
+
vitals = await withTimeout(
|
|
328
|
+
collectVitals(client, sessionId, vitalsWaitMs),
|
|
329
|
+
vitalsWaitMs + STEP_TIMEOUT_MS,
|
|
330
|
+
'web vitals',
|
|
331
|
+
);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
notes.push(`Web vitals unavailable: ${message(err)}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const a11y = await collectA11y(client, sessionId, notes);
|
|
337
|
+
|
|
338
|
+
// Stop before the screenshots: a device matrix re-navigates the page
|
|
339
|
+
// several times, and those frames belong to no failure worth explaining.
|
|
340
|
+
if (recorder) await recorder.stop().catch(() => {});
|
|
341
|
+
|
|
342
|
+
const finalUrl = await getCurrentUrl(client, sessionId).catch(() => options.url);
|
|
343
|
+
const title = await getCurrentTitle(client, sessionId).catch(() => '');
|
|
344
|
+
|
|
345
|
+
const consoleEntries = getConsoleMessages(sessionId);
|
|
346
|
+
const pageErrors = getPageErrors(sessionId);
|
|
347
|
+
const requests = toRequestEntries(
|
|
348
|
+
getCapturedRequests(sessionId),
|
|
349
|
+
tracker.failures,
|
|
350
|
+
clock.offset(),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
const inlineBudget = { left: MAX_INLINE_BYTES };
|
|
354
|
+
const screenshots: Screenshot[] = [];
|
|
355
|
+
const main = await takeShot(
|
|
356
|
+
client,
|
|
357
|
+
sessionId,
|
|
358
|
+
'page',
|
|
359
|
+
fullPage,
|
|
360
|
+
options.screenshotDir,
|
|
361
|
+
notes,
|
|
362
|
+
inlineBudget,
|
|
363
|
+
);
|
|
364
|
+
if (main) screenshots.push(main);
|
|
365
|
+
|
|
366
|
+
if (options.devices?.length) {
|
|
367
|
+
screenshots.push(
|
|
368
|
+
...(await captureDeviceMatrix(
|
|
369
|
+
client,
|
|
370
|
+
sessionId,
|
|
371
|
+
options.url,
|
|
372
|
+
options.devices,
|
|
373
|
+
options.screenshotDir,
|
|
374
|
+
notes,
|
|
375
|
+
inlineBudget,
|
|
376
|
+
)),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
url: options.url,
|
|
382
|
+
finalUrl,
|
|
383
|
+
title,
|
|
384
|
+
capturedAt,
|
|
385
|
+
durationMs: Date.now() - startedAt,
|
|
386
|
+
console: consoleEntries,
|
|
387
|
+
pageErrors,
|
|
388
|
+
requests,
|
|
389
|
+
vitals,
|
|
390
|
+
a11y: a11y.findings,
|
|
391
|
+
structure: a11y.structure,
|
|
392
|
+
screenshots,
|
|
393
|
+
notes,
|
|
394
|
+
recording: recorder
|
|
395
|
+
? {
|
|
396
|
+
startedAtMs: startedAt,
|
|
397
|
+
frames: recorder.frames,
|
|
398
|
+
droppedFrames: recorder.droppedFrames,
|
|
399
|
+
requested: true,
|
|
400
|
+
}
|
|
401
|
+
: undefined,
|
|
402
|
+
};
|
|
403
|
+
} finally {
|
|
404
|
+
tracker.stop();
|
|
405
|
+
clock.stop();
|
|
406
|
+
if (recorder) await recorder.stop().catch(() => {});
|
|
407
|
+
}
|
|
408
|
+
}
|