@warlock.js/core 5.32.0 → 5.34.0
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/CHANGELOG.md +64 -46
- package/esm/application/application-config-types.d.mts +8 -0
- package/esm/application/application-config-types.d.mts.map +1 -1
- package/esm/application/boot-signal.d.mts +8 -8
- package/esm/application/boot-signal.d.mts.map +1 -1
- package/esm/application/boot-signal.mjs +15 -9
- package/esm/application/boot-signal.mjs.map +1 -1
- package/esm/cli/commands/ai/ai-command.action.mjs +140 -0
- package/esm/cli/commands/ai/ai-command.action.mjs.map +1 -0
- package/esm/cli/commands/ai.command.mjs +50 -0
- package/esm/cli/commands/ai.command.mjs.map +1 -0
- package/esm/cli/commands/check/check-command.action.mjs +4 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +6 -1
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/e2e.command.mjs +63 -0
- package/esm/cli/commands/e2e.command.mjs.map +1 -0
- package/esm/cli/commands/load-e2e-spec.mjs +47 -0
- package/esm/cli/commands/load-e2e-spec.mjs.map +1 -0
- package/esm/cli/commands/pm2/pm2-command.action.mjs +91 -0
- package/esm/cli/commands/pm2/pm2-command.action.mjs.map +1 -0
- package/esm/cli/commands/pm2/pm2-config.mjs +115 -0
- package/esm/cli/commands/pm2/pm2-config.mjs.map +1 -0
- package/esm/cli/commands/pm2.command.mjs +53 -0
- package/esm/cli/commands/pm2.command.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +6 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +1 -4
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/shutdown-timeout.mjs +7 -0
- package/esm/connectors/shutdown-timeout.mjs.map +1 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +8 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/dev-server/health-checker/checkers/oxlint-health-checker.mjs +1 -1
- package/esm/dev-server/health-checker/default-health-checkers.mjs +2 -16
- package/esm/dev-server/health-checker/default-health-checkers.mjs.map +1 -1
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +1 -2
- package/esm/dev-server/health-checker/file-health-checker.contract.d.mts.map +1 -1
- package/esm/dev-server/health-checker/file-health-result.d.mts +1 -1
- package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
- package/esm/e2e/browser.d.mts +4 -0
- package/esm/e2e/browser.d.mts.map +1 -0
- package/esm/e2e/browser.mjs +88 -0
- package/esm/e2e/browser.mjs.map +1 -0
- package/esm/e2e/cdp.d.mts +152 -0
- package/esm/e2e/cdp.d.mts.map +1 -0
- package/esm/e2e/cdp.mjs +106 -0
- package/esm/e2e/cdp.mjs.map +1 -0
- package/esm/e2e/fixture.mjs +195 -0
- package/esm/e2e/fixture.mjs.map +1 -0
- package/esm/e2e/index.d.mts +5 -0
- package/esm/e2e/index.mjs +6 -0
- package/esm/e2e/owned-process.mjs +193 -0
- package/esm/e2e/owned-process.mjs.map +1 -0
- package/esm/e2e/page.d.mts +64 -0
- package/esm/e2e/page.d.mts.map +1 -0
- package/esm/e2e/page.mjs +376 -0
- package/esm/e2e/page.mjs.map +1 -0
- package/esm/e2e/run-e2e.d.mts +17 -0
- package/esm/e2e/run-e2e.d.mts.map +1 -0
- package/esm/e2e/run-e2e.mjs +143 -0
- package/esm/e2e/run-e2e.mjs.map +1 -0
- package/esm/e2e/types.d.mts +123 -0
- package/esm/e2e/types.d.mts.map +1 -0
- package/esm/e2e/types.mjs +24 -0
- package/esm/e2e/types.mjs.map +1 -0
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/process.feature.mjs +65 -0
- package/esm/generations/features/process.feature.mjs.map +1 -0
- package/esm/http/port-preflight.d.mts.map +1 -1
- package/esm/http/port-preflight.mjs +2 -0
- package/esm/http/port-preflight.mjs.map +1 -1
- package/esm/index.d.mts +7 -2
- package/esm/index.mjs +7 -2
- package/esm/secrets/errors.d.mts +9 -0
- package/esm/secrets/errors.d.mts.map +1 -0
- package/esm/secrets/errors.mjs +24 -0
- package/esm/secrets/errors.mjs.map +1 -0
- package/esm/secrets/file-backend.mjs +167 -0
- package/esm/secrets/file-backend.mjs.map +1 -0
- package/esm/secrets/index.d.mts +6 -0
- package/esm/secrets/index.mjs +6 -0
- package/esm/secrets/keyring-backend.mjs +50 -0
- package/esm/secrets/keyring-backend.mjs.map +1 -0
- package/esm/secrets/memory-backend.d.mts +12 -0
- package/esm/secrets/memory-backend.d.mts.map +1 -0
- package/esm/secrets/memory-backend.mjs +28 -0
- package/esm/secrets/memory-backend.mjs.map +1 -0
- package/esm/secrets/resolve-driver.mjs +14 -0
- package/esm/secrets/resolve-driver.mjs.map +1 -0
- package/esm/secrets/secret-store.d.mts +25 -0
- package/esm/secrets/secret-store.d.mts.map +1 -0
- package/esm/secrets/secret-store.mjs +120 -0
- package/esm/secrets/secret-store.mjs.map +1 -0
- package/esm/secrets/secrets.d.mts +11 -0
- package/esm/secrets/secrets.d.mts.map +1 -0
- package/esm/secrets/secrets.mjs +72 -0
- package/esm/secrets/secrets.mjs.map +1 -0
- package/esm/secrets/types.d.mts +26 -0
- package/esm/secrets/types.d.mts.map +1 -0
- package/esm/socket/index.d.mts +2 -2
- package/esm/socket/index.mjs +2 -2
- package/esm/socket/utils.d.mts +7 -0
- package/esm/socket/utils.d.mts.map +1 -1
- package/esm/socket/utils.mjs +12 -1
- package/esm/socket/utils.mjs.map +1 -1
- package/esm/tests/fakes/index.d.mts +2 -1
- package/esm/tests/fakes/index.mjs +2 -1
- package/esm/tests/fakes/secrets-fake.d.mts +13 -0
- package/esm/tests/fakes/secrets-fake.d.mts.map +1 -0
- package/esm/tests/fakes/secrets-fake.mjs +50 -0
- package/esm/tests/fakes/secrets-fake.mjs.map +1 -0
- package/esm/tests/fakes/test-fakes.d.mts +2 -2
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +5 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +2 -1
- package/esm/tests/index.mjs +2 -1
- package/esm/tests/vitest-setup.d.mts +1 -1
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/lint-step.d.mts +2 -8
- package/esm/tooling/check/lint-step.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.mjs +1 -42
- package/esm/tooling/check/lint-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +3 -1
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +1 -1
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/updater/package-manager.mjs +5 -1
- package/esm/updater/package-manager.mjs.map +1 -1
- package/esm/upgrade/codemods/index.mjs +3 -0
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/oxc-toolchain.mjs +191 -0
- package/esm/upgrade/codemods/oxc-toolchain.mjs.map +1 -0
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs +6 -0
- package/esm/upgrade/codemods/oxlint-rule-schemas.mjs.map +1 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs +343 -0
- package/esm/upgrade/codemods/port-eslint-rules.mjs.map +1 -0
- package/esm/upgrade/codemods/toolchain-templates.mjs +244 -0
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -0
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs +60 -0
- package/esm/upgrade/codemods/validate-oxlint-rule.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +14 -7
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/upgrade/run-upgrade.mjs +25 -6
- package/esm/upgrade/run-upgrade.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +1 -1
- package/llms-full.txt +538 -29
- package/llms.txt +5 -3
- package/package.json +29 -20
- package/skills/README.md +2 -1
- package/skills/create-module/SKILL.md +2 -2
- package/skills/index.md +1 -1
- package/skills/run-app/SKILL.md +115 -6
- package/skills/store-secrets/SKILL.md +141 -0
- package/skills/test-browser/SKILL.md +119 -0
- package/skills/test-fakes/SKILL.md +13 -11
- package/skills/upgrade-from-5-29/SKILL.md +85 -7
- package/skills/warlock-check/SKILL.md +7 -3
- package/skills/warlock-doctor/SKILL.md +8 -0
- package/skills/wire-socket/SKILL.md +19 -0
- package/skills/write-cli-command/SKILL.md +21 -0
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +0 -154
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.d.mts +0 -1
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +0 -192
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +0 -1
package/esm/e2e/page.mjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { defaultViewports, errorMessage, matches } from "./types.mjs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
//#region ../../packages/core/src/e2e/page.ts
|
|
7
|
+
/** Runs inside the browser. Two paints plus cross-probe box comparison detect
|
|
8
|
+
* scroll/animation/layout races. The hit test also accepts target descendants.
|
|
9
|
+
*/
|
|
10
|
+
function clickProbe(selector) {
|
|
11
|
+
return (async () => {
|
|
12
|
+
const empty = (reason) => ({
|
|
13
|
+
x: 0,
|
|
14
|
+
y: 0,
|
|
15
|
+
width: 0,
|
|
16
|
+
height: 0,
|
|
17
|
+
reason
|
|
18
|
+
});
|
|
19
|
+
const element = document.querySelector(selector);
|
|
20
|
+
if (!(element instanceof HTMLElement || element instanceof SVGElement)) return empty("target is missing or is not an HTML/SVG element");
|
|
21
|
+
element.scrollIntoView({
|
|
22
|
+
block: "center",
|
|
23
|
+
inline: "center",
|
|
24
|
+
behavior: "instant"
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
27
|
+
if (!element.isConnected || document.querySelector(selector) !== element) return empty("target was replaced during layout");
|
|
28
|
+
const box = element.getBoundingClientRect();
|
|
29
|
+
const x = box.x + box.width / 2;
|
|
30
|
+
const y = box.y + box.height / 2;
|
|
31
|
+
const style = getComputedStyle(element);
|
|
32
|
+
let reason = "";
|
|
33
|
+
if (box.width <= 0 || box.height <= 0 || style.visibility !== "visible" || style.display === "none" || Number(style.opacity) === 0) reason = "target is hidden or has an empty box";
|
|
34
|
+
else if (element.matches(":disabled") || element.closest("[inert], [aria-disabled=\"true\"]")) reason = "target is disabled or inert";
|
|
35
|
+
else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) reason = "target center is outside the viewport";
|
|
36
|
+
else {
|
|
37
|
+
const top = document.elementFromPoint(x, y);
|
|
38
|
+
if (!top || !element.contains(top)) reason = `target is covered by ${top?.tagName ?? "no element"}${top?.id ? "#" + top.id : ""}`;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
x,
|
|
42
|
+
y,
|
|
43
|
+
width: box.width,
|
|
44
|
+
height: box.height,
|
|
45
|
+
reason
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
}
|
|
49
|
+
function boxesStable(first, second) {
|
|
50
|
+
return !first.reason && !second.reason && [
|
|
51
|
+
"x",
|
|
52
|
+
"y",
|
|
53
|
+
"width",
|
|
54
|
+
"height"
|
|
55
|
+
].every((key) => Math.abs(first[key] - second[key]) < .25);
|
|
56
|
+
}
|
|
57
|
+
var BrowserPage = class {
|
|
58
|
+
constructor(cdp, origin, outputDirectory, evidence, spec) {
|
|
59
|
+
this.cdp = cdp;
|
|
60
|
+
this.origin = origin;
|
|
61
|
+
this.outputDirectory = outputDirectory;
|
|
62
|
+
this.evidence = evidence;
|
|
63
|
+
this.spec = spec;
|
|
64
|
+
this.viewport = { ...defaultViewports[0] };
|
|
65
|
+
this.loads = 0;
|
|
66
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
67
|
+
this.requestUrls = /* @__PURE__ */ new Map();
|
|
68
|
+
this.lastNetworkActivity = Date.now();
|
|
69
|
+
cdp.on("Page.loadEventFired", () => {
|
|
70
|
+
this.loads++;
|
|
71
|
+
});
|
|
72
|
+
cdp.on("Runtime.consoleAPICalled", ({ type, args }) => evidence.console.push({
|
|
73
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
level: type === "assert" ? "error" : type,
|
|
75
|
+
text: args.map((arg) => String(arg.value ?? arg.description ?? "")).join(" ")
|
|
76
|
+
}));
|
|
77
|
+
cdp.on("Runtime.exceptionThrown", ({ exceptionDetails }) => evidence.console.push({
|
|
78
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79
|
+
level: "error",
|
|
80
|
+
text: exceptionDetails.exception?.description ?? exceptionDetails.text
|
|
81
|
+
}));
|
|
82
|
+
cdp.on("Log.entryAdded", ({ entry }) => evidence.console.push({
|
|
83
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
84
|
+
level: entry.level,
|
|
85
|
+
text: entry.text
|
|
86
|
+
}));
|
|
87
|
+
cdp.on("Network.requestWillBeSent", ({ requestId, request }) => {
|
|
88
|
+
this.pendingRequests.set(requestId, request.url);
|
|
89
|
+
this.requestUrls.set(requestId, request.url);
|
|
90
|
+
this.lastNetworkActivity = Date.now();
|
|
91
|
+
});
|
|
92
|
+
cdp.on("Network.responseReceived", ({ requestId, response }) => {
|
|
93
|
+
this.requestUrls.set(requestId, response.url);
|
|
94
|
+
if (response.status >= 400) evidence.networkFailures.push({
|
|
95
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
url: response.url,
|
|
97
|
+
status: response.status
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
cdp.on("Network.loadingFailed", ({ requestId, errorText, canceled }) => {
|
|
101
|
+
evidence.networkFailures.push({
|
|
102
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
103
|
+
url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,
|
|
104
|
+
errorText,
|
|
105
|
+
canceled
|
|
106
|
+
});
|
|
107
|
+
this.completeRequest(requestId);
|
|
108
|
+
});
|
|
109
|
+
cdp.on("Network.loadingFinished", ({ requestId }) => this.completeRequest(requestId));
|
|
110
|
+
}
|
|
111
|
+
completeRequest(requestId) {
|
|
112
|
+
this.pendingRequests.delete(requestId);
|
|
113
|
+
this.requestUrls.delete(requestId);
|
|
114
|
+
this.lastNetworkActivity = Date.now();
|
|
115
|
+
}
|
|
116
|
+
async initialize() {
|
|
117
|
+
await this.cdp.command("Page.enable", {});
|
|
118
|
+
await this.cdp.command("Runtime.enable", {});
|
|
119
|
+
await this.cdp.command("Network.enable", {});
|
|
120
|
+
await this.cdp.command("Log.enable", {});
|
|
121
|
+
await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);
|
|
122
|
+
}
|
|
123
|
+
async step(name, action) {
|
|
124
|
+
const start = performance.now();
|
|
125
|
+
const entry = {
|
|
126
|
+
name,
|
|
127
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
+
durationMs: 0,
|
|
129
|
+
status: "passed",
|
|
130
|
+
error: void 0
|
|
131
|
+
};
|
|
132
|
+
this.evidence.steps.push(entry);
|
|
133
|
+
try {
|
|
134
|
+
return await action();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
entry.status = "failed";
|
|
137
|
+
entry.error = errorMessage(error);
|
|
138
|
+
throw error;
|
|
139
|
+
} finally {
|
|
140
|
+
entry.durationMs = Math.round(performance.now() - start);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async evaluate(expression, timeoutMs = 1e4) {
|
|
144
|
+
const response = await this.cdp.command("Runtime.evaluate", {
|
|
145
|
+
expression,
|
|
146
|
+
returnByValue: true,
|
|
147
|
+
awaitPromise: true,
|
|
148
|
+
silent: true
|
|
149
|
+
}, timeoutMs);
|
|
150
|
+
if (response.exceptionDetails) throw new Error(`Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`);
|
|
151
|
+
return response.result.value;
|
|
152
|
+
}
|
|
153
|
+
async poll(description, check, timeoutMs) {
|
|
154
|
+
const deadline = Date.now() + timeoutMs;
|
|
155
|
+
let reason = "condition stayed false";
|
|
156
|
+
while (Date.now() < deadline) {
|
|
157
|
+
try {
|
|
158
|
+
if (await check(Math.max(1, deadline - Date.now()))) return;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
reason = errorMessage(error);
|
|
161
|
+
}
|
|
162
|
+
await setTimeout(Math.min(50, Math.max(0, deadline - Date.now())));
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);
|
|
165
|
+
}
|
|
166
|
+
async goto(path, timeoutMs = 1e4) {
|
|
167
|
+
await this.step(`goto ${path}`, async () => {
|
|
168
|
+
const url = new URL(path, this.origin).href;
|
|
169
|
+
const before = this.loads;
|
|
170
|
+
const navigation = await this.cdp.command("Page.navigate", { url }, timeoutMs);
|
|
171
|
+
if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);
|
|
172
|
+
await this.poll(`Navigation to ${url}`, (remaining) => this.evaluate(`document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : "true"}`, remaining), timeoutMs);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async waitFor(options) {
|
|
176
|
+
if (options.selector === void 0 && options.text === void 0 && options.url === void 0) throw new Error("waitFor requires a selector, text, or URL");
|
|
177
|
+
await this.step(`waitFor ${JSON.stringify(options)}`, () => this.poll("waitFor " + JSON.stringify(options), (remaining) => this.evaluate(`(() => {
|
|
178
|
+
const options = ${JSON.stringify(options)};
|
|
179
|
+
const element = options.selector ? document.querySelector(options.selector) : document.body;
|
|
180
|
+
if (!element) return false;
|
|
181
|
+
if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }
|
|
182
|
+
return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&
|
|
183
|
+
(options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);
|
|
184
|
+
})()`, remaining), options.timeoutMs ?? 1e4));
|
|
185
|
+
}
|
|
186
|
+
/** Arms the document/URL comparison BEFORE invoking the navigation action,
|
|
187
|
+
* covering full loads, reloads, hash changes and client-side routing.
|
|
188
|
+
*/
|
|
189
|
+
async waitForNavigation(action, options = {}) {
|
|
190
|
+
await this.step("waitForNavigation", async () => {
|
|
191
|
+
const before = await this.evaluate("({href:location.href,timeOrigin:performance.timeOrigin})");
|
|
192
|
+
await action();
|
|
193
|
+
await this.poll("Navigation", (remaining) => this.evaluate(`document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : "true"}`, remaining), options.timeoutMs ?? 1e4);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async click(selector, timeoutMs = 1e4) {
|
|
197
|
+
await this.step(`click ${selector}`, async () => {
|
|
198
|
+
const deadline = Date.now() + timeoutMs;
|
|
199
|
+
let previous;
|
|
200
|
+
let reason = "layout has not settled";
|
|
201
|
+
while (Date.now() < deadline) {
|
|
202
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
203
|
+
let box;
|
|
204
|
+
try {
|
|
205
|
+
box = await this.evaluate(`(${clickProbe.toString()})(${JSON.stringify(selector)})`, remaining());
|
|
206
|
+
} catch (error) {
|
|
207
|
+
reason = errorMessage(error);
|
|
208
|
+
previous = void 0;
|
|
209
|
+
await setTimeout(25);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
reason = box.reason || "target box is moving";
|
|
213
|
+
if (previous && boxesStable(previous, box)) {
|
|
214
|
+
let hovered;
|
|
215
|
+
try {
|
|
216
|
+
await this.cdp.command("Input.dispatchMouseEvent", {
|
|
217
|
+
type: "mouseMoved",
|
|
218
|
+
x: box.x,
|
|
219
|
+
y: box.y
|
|
220
|
+
}, remaining());
|
|
221
|
+
hovered = await this.evaluate(`(${clickProbe.toString()})(${JSON.stringify(selector)})`, remaining());
|
|
222
|
+
} catch (error) {
|
|
223
|
+
reason = errorMessage(error);
|
|
224
|
+
previous = void 0;
|
|
225
|
+
await setTimeout(Math.min(25, remaining()));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (Date.now() < deadline && boxesStable(box, hovered)) {
|
|
229
|
+
await this.cdp.command("Input.dispatchMouseEvent", {
|
|
230
|
+
type: "mousePressed",
|
|
231
|
+
x: hovered.x,
|
|
232
|
+
y: hovered.y,
|
|
233
|
+
button: "left",
|
|
234
|
+
buttons: 1,
|
|
235
|
+
clickCount: 1
|
|
236
|
+
}, remaining());
|
|
237
|
+
await this.cdp.command("Input.dispatchMouseEvent", {
|
|
238
|
+
type: "mouseReleased",
|
|
239
|
+
x: hovered.x,
|
|
240
|
+
y: hovered.y,
|
|
241
|
+
button: "left",
|
|
242
|
+
buttons: 0,
|
|
243
|
+
clickCount: 1
|
|
244
|
+
}, remaining());
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
reason = hovered.reason || "hover changed the target box";
|
|
248
|
+
previous = void 0;
|
|
249
|
+
} else previous = box;
|
|
250
|
+
await setTimeout(Math.min(50, remaining()));
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async fill(selector, value) {
|
|
256
|
+
await this.step(`fill ${selector}`, async () => {
|
|
257
|
+
await this.click(selector);
|
|
258
|
+
await this.evaluate(`(() => {
|
|
259
|
+
const element=document.querySelector(${JSON.stringify(selector)});
|
|
260
|
+
if (element !== document.activeElement) throw new Error('Fill target lost focus');
|
|
261
|
+
if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {
|
|
262
|
+
const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
|
|
263
|
+
const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;
|
|
264
|
+
if (!setter) throw new Error('Fill target has no native value setter');
|
|
265
|
+
setter.call(element,'');
|
|
266
|
+
} else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';
|
|
267
|
+
else throw new Error('Fill target must be an editable input, textarea or contenteditable');
|
|
268
|
+
element.dispatchEvent(new Event('input',{bubbles:true}));
|
|
269
|
+
})()`);
|
|
270
|
+
await this.evaluate(`document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error("Fill target lost focus after input"); })()`);
|
|
271
|
+
if (value) await this.cdp.command("Input.insertText", { text: value });
|
|
272
|
+
await this.evaluate("document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))");
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/** Appends Unicode text through the browser's text-input path. */
|
|
276
|
+
async type(selector, text) {
|
|
277
|
+
await this.step(`type ${selector}`, async () => {
|
|
278
|
+
await this.click(selector);
|
|
279
|
+
if (await this.evaluate(`(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== "number") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`)) {
|
|
280
|
+
await this.cdp.command("Input.dispatchKeyEvent", {
|
|
281
|
+
type: "keyDown",
|
|
282
|
+
key: "End",
|
|
283
|
+
code: "End",
|
|
284
|
+
windowsVirtualKeyCode: 35
|
|
285
|
+
});
|
|
286
|
+
await this.cdp.command("Input.dispatchKeyEvent", {
|
|
287
|
+
type: "keyUp",
|
|
288
|
+
key: "End",
|
|
289
|
+
code: "End",
|
|
290
|
+
windowsVirtualKeyCode: 35
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
await this.cdp.command("Input.insertText", { text });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
async setViewport(viewport) {
|
|
297
|
+
if (!/^[a-z0-9_-]+$/i.test(viewport.name) || !Number.isInteger(viewport.width) || !Number.isInteger(viewport.height) || viewport.width <= 0 || viewport.height <= 0) throw new Error("Viewport requires a safe name and positive integer width/height");
|
|
298
|
+
await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {
|
|
299
|
+
await this.cdp.command("Emulation.setDeviceMetricsOverride", {
|
|
300
|
+
width: viewport.width,
|
|
301
|
+
height: viewport.height,
|
|
302
|
+
deviceScaleFactor: 1,
|
|
303
|
+
mobile: viewport.mobile ?? false
|
|
304
|
+
});
|
|
305
|
+
this.viewport = { ...viewport };
|
|
306
|
+
await this.evaluate("new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))");
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async assertViewport() {
|
|
310
|
+
return this.step(`assertViewport ${this.viewport.name}`, async () => {
|
|
311
|
+
const report = await this.evaluate(`({
|
|
312
|
+
requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,
|
|
313
|
+
offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})
|
|
314
|
+
.filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})
|
|
315
|
+
.sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)
|
|
316
|
+
})`);
|
|
317
|
+
if (report.innerWidth !== report.requestedWidth || report.documentWidth > report.requestedWidth || report.bodyWidth > report.requestedWidth) throw new Error(`Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`);
|
|
318
|
+
return report;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
async screenshot(name) {
|
|
322
|
+
if (!/^[a-z0-9_-]+$/i.test(name)) throw new Error("Screenshot names must contain only letters, digits, '_' or '-'");
|
|
323
|
+
return this.step(`screenshot ${name}`, async () => {
|
|
324
|
+
const file = `${String(this.evidence.screenshots.length + 1).padStart(3, "0")}-${name}-${this.viewport.name}.png`;
|
|
325
|
+
const { data } = await this.cdp.command("Page.captureScreenshot", { format: "png" });
|
|
326
|
+
await writeFile(join(this.outputDirectory, file), Buffer.from(data, "base64"));
|
|
327
|
+
this.evidence.screenshots.push({
|
|
328
|
+
name,
|
|
329
|
+
file,
|
|
330
|
+
viewport: { ...this.viewport }
|
|
331
|
+
});
|
|
332
|
+
return join(this.outputDirectory, file);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async captureViewports(name, viewports = this.spec.viewports ?? defaultViewports) {
|
|
336
|
+
const failures = [];
|
|
337
|
+
const original = this.viewport;
|
|
338
|
+
for (const viewport of viewports) try {
|
|
339
|
+
await this.setViewport(viewport);
|
|
340
|
+
try {
|
|
341
|
+
await this.assertViewport();
|
|
342
|
+
} finally {
|
|
343
|
+
await this.screenshot(name);
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
failures.push(error);
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
await this.setViewport(original);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
failures.push(error);
|
|
352
|
+
}
|
|
353
|
+
if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join("; "));
|
|
354
|
+
}
|
|
355
|
+
async waitForNetworkIdle(timeoutMs = 1e4) {
|
|
356
|
+
await this.step("waitForNetworkIdle", () => this.poll("Network idle", async () => {
|
|
357
|
+
const pending = [...this.pendingRequests.values()].filter((url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)));
|
|
358
|
+
if (pending.length) throw new Error(`Pending requests: ${pending.join(", ")}`);
|
|
359
|
+
return Date.now() - this.lastNetworkActivity >= 300;
|
|
360
|
+
}, timeoutMs));
|
|
361
|
+
}
|
|
362
|
+
assertHealthy() {
|
|
363
|
+
for (const entry of this.evidence.console) if (entry.level === "error") entry.allowed = this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;
|
|
364
|
+
for (const entry of this.evidence.networkFailures) entry.allowed = this.spec.allowNetworkFailures?.some((rule) => matches(rule.url, entry.url) && (rule.status === void 0 || rule.status === entry.status) && (rule.errorText === void 0 || matches(rule.errorText, entry.errorText ?? ""))) ?? false;
|
|
365
|
+
const consoleErrors = this.evidence.console.filter((entry) => entry.level === "error" && !entry.allowed);
|
|
366
|
+
const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);
|
|
367
|
+
if (consoleErrors.length || networkErrors.length) throw new Error(`Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\n${JSON.stringify({
|
|
368
|
+
console: consoleErrors,
|
|
369
|
+
network: networkErrors
|
|
370
|
+
})}`);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
export { BrowserPage, boxesStable };
|
|
376
|
+
//# sourceMappingURL=page.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/page.ts"],"sourcesContent":["import { writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { CdpConnection } from \"./cdp\";\nimport {\n defaultViewports,\n errorMessage,\n matches,\n type E2EEvidence,\n type E2ESpec,\n type Viewport,\n} from \"./types\";\n\ntype WaitOptions = { selector?: string; text?: string; url?: string; timeoutMs?: number };\ntype ClickBox = { x: number; y: number; width: number; height: number; reason: string };\ntype ViewportReport = {\n requestedWidth: number;\n innerWidth: number;\n documentWidth: number;\n bodyWidth: number;\n offenders: {\n tag: string;\n id: string;\n className: string;\n width: number;\n scrollWidth: number;\n right: number;\n left: number;\n }[];\n};\n\n/** Runs inside the browser. Two paints plus cross-probe box comparison detect\n * scroll/animation/layout races. The hit test also accepts target descendants.\n */\nfunction clickProbe(selector: string): Promise<ClickBox> {\n return (async () => {\n const empty = (reason: string): ClickBox => ({ x: 0, y: 0, width: 0, height: 0, reason });\n const element = document.querySelector(selector);\n if (!(element instanceof HTMLElement || element instanceof SVGElement))\n return empty(\"target is missing or is not an HTML/SVG element\");\n element.scrollIntoView({ block: \"center\", inline: \"center\", behavior: \"instant\" });\n await new Promise<void>((resolve) =>\n requestAnimationFrame(() => requestAnimationFrame(() => resolve())),\n );\n if (!element.isConnected || document.querySelector(selector) !== element)\n return empty(\"target was replaced during layout\");\n const box = element.getBoundingClientRect();\n const x = box.x + box.width / 2;\n const y = box.y + box.height / 2;\n const style = getComputedStyle(element);\n let reason = \"\";\n if (\n box.width <= 0 ||\n box.height <= 0 ||\n style.visibility !== \"visible\" ||\n style.display === \"none\" ||\n Number(style.opacity) === 0\n )\n reason = \"target is hidden or has an empty box\";\n else if (element.matches(\":disabled\") || element.closest('[inert], [aria-disabled=\"true\"]'))\n reason = \"target is disabled or inert\";\n else if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight)\n reason = \"target center is outside the viewport\";\n else {\n const top = document.elementFromPoint(x, y);\n if (!top || !element.contains(top))\n reason = `target is covered by ${top?.tagName ?? \"no element\"}${top?.id ? \"#\" + top.id : \"\"}`;\n }\n return { x, y, width: box.width, height: box.height, reason };\n })();\n}\n\nexport function boxesStable(first: ClickBox, second: ClickBox): boolean {\n return (\n !first.reason &&\n !second.reason &&\n ([\"x\", \"y\", \"width\", \"height\"] as const).every(\n (key) => Math.abs(first[key] - second[key]) < 0.25,\n )\n );\n}\n\nexport class BrowserPage {\n private viewport: Viewport = { ...defaultViewports[0] };\n private loads = 0;\n private pendingRequests = new Map<string, string>();\n private requestUrls = new Map<string, string>();\n private lastNetworkActivity = Date.now();\n\n constructor(\n private cdp: CdpConnection,\n private origin: string,\n private outputDirectory: string,\n private evidence: E2EEvidence,\n private spec: E2ESpec,\n ) {\n cdp.on(\"Page.loadEventFired\", () => {\n this.loads++;\n });\n cdp.on(\"Runtime.consoleAPICalled\", ({ type, args }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: type === \"assert\" ? \"error\" : type,\n text: args.map((arg) => String(arg.value ?? arg.description ?? \"\")).join(\" \"),\n }),\n );\n cdp.on(\"Runtime.exceptionThrown\", ({ exceptionDetails }) =>\n evidence.console.push({\n at: new Date().toISOString(),\n level: \"error\",\n text: exceptionDetails.exception?.description ?? exceptionDetails.text,\n }),\n );\n cdp.on(\"Log.entryAdded\", ({ entry }) =>\n evidence.console.push({ at: new Date().toISOString(), level: entry.level, text: entry.text }),\n );\n cdp.on(\"Network.requestWillBeSent\", ({ requestId, request }) => {\n this.pendingRequests.set(requestId, request.url);\n this.requestUrls.set(requestId, request.url);\n this.lastNetworkActivity = Date.now();\n });\n cdp.on(\"Network.responseReceived\", ({ requestId, response }) => {\n this.requestUrls.set(requestId, response.url);\n if (response.status >= 400)\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: response.url,\n status: response.status,\n });\n });\n cdp.on(\"Network.loadingFailed\", ({ requestId, errorText, canceled }) => {\n evidence.networkFailures.push({\n at: new Date().toISOString(),\n url: this.requestUrls.get(requestId) ?? `unknown request ${requestId}`,\n errorText,\n canceled,\n });\n this.completeRequest(requestId);\n });\n cdp.on(\"Network.loadingFinished\", ({ requestId }) => this.completeRequest(requestId));\n }\n\n private completeRequest(requestId: string): void {\n this.pendingRequests.delete(requestId);\n this.requestUrls.delete(requestId);\n this.lastNetworkActivity = Date.now();\n }\n\n async initialize(): Promise<void> {\n await this.cdp.command(\"Page.enable\", {});\n await this.cdp.command(\"Runtime.enable\", {});\n await this.cdp.command(\"Network.enable\", {});\n await this.cdp.command(\"Log.enable\", {});\n await this.setViewport(this.spec.viewports?.[0] ?? defaultViewports[0]);\n }\n\n async step<T>(name: string, action: () => Promise<T>): Promise<T> {\n const start = performance.now();\n const entry = {\n name,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"passed\" as \"passed\" | \"failed\",\n error: undefined as string | undefined,\n };\n this.evidence.steps.push(entry);\n try {\n return await action();\n } catch (error) {\n entry.status = \"failed\";\n entry.error = errorMessage(error);\n throw error;\n } finally {\n entry.durationMs = Math.round(performance.now() - start);\n }\n }\n\n async evaluate<T = unknown>(expression: string, timeoutMs = 10_000): Promise<T> {\n const response = await this.cdp.command(\n \"Runtime.evaluate\",\n { expression, returnByValue: true, awaitPromise: true, silent: true },\n timeoutMs,\n );\n if (response.exceptionDetails)\n throw new Error(\n `Browser evaluation failed: ${response.exceptionDetails.exception?.description ?? response.exceptionDetails.text}`,\n );\n return response.result.value as T;\n }\n\n private async poll(\n description: string,\n check: (remainingMs: number) => Promise<boolean>,\n timeoutMs: number,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n let reason = \"condition stayed false\";\n while (Date.now() < deadline) {\n try {\n if (await check(Math.max(1, deadline - Date.now()))) return;\n } catch (error) {\n reason = errorMessage(error);\n }\n await delay(Math.min(50, Math.max(0, deadline - Date.now())));\n }\n throw new Error(`${description} timed out after ${timeoutMs}ms: ${reason}`);\n }\n\n async goto(path: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`goto ${path}`, async () => {\n const url = new URL(path, this.origin).href;\n const before = this.loads;\n const navigation = await this.cdp.command(\"Page.navigate\", { url }, timeoutMs);\n if (navigation.errorText) throw new Error(`Navigation failed: ${navigation.errorText}`);\n await this.poll(\n `Navigation to ${url}`,\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && ${navigation.loaderId ? this.loads > before : \"true\"}`,\n remaining,\n ),\n timeoutMs,\n );\n });\n }\n\n async waitFor(options: WaitOptions): Promise<void> {\n if (options.selector === undefined && options.text === undefined && options.url === undefined)\n throw new Error(\"waitFor requires a selector, text, or URL\");\n await this.step(`waitFor ${JSON.stringify(options)}`, () =>\n this.poll(\n \"waitFor \" + JSON.stringify(options),\n (remaining) =>\n this.evaluate<boolean>(\n `(() => {\n const options = ${JSON.stringify(options)};\n const element = options.selector ? document.querySelector(options.selector) : document.body;\n if (!element) return false;\n if (options.selector) { const box=element.getBoundingClientRect(); const css=getComputedStyle(element); if (!box.width || !box.height || css.visibility !== 'visible') return false; }\n return (options.text === undefined || (element.innerText ?? element.textContent ?? '').includes(options.text)) &&\n (options.url === undefined || location.href === new URL(options.url, ${JSON.stringify(this.origin)}).href);\n })()`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n ),\n );\n }\n\n /** Arms the document/URL comparison BEFORE invoking the navigation action,\n * covering full loads, reloads, hash changes and client-side routing.\n */\n async waitForNavigation(\n action: () => Promise<unknown>,\n options: { url?: string; timeoutMs?: number } = {},\n ): Promise<void> {\n await this.step(\"waitForNavigation\", async () => {\n const before = await this.evaluate<{ href: string; timeOrigin: number }>(\n \"({href:location.href,timeOrigin:performance.timeOrigin})\",\n );\n await action();\n await this.poll(\n \"Navigation\",\n (remaining) =>\n this.evaluate<boolean>(\n `document.readyState === 'complete' && (location.href !== ${JSON.stringify(before.href)} || performance.timeOrigin !== ${before.timeOrigin}) && ${options.url ? `location.href === ${JSON.stringify(new URL(options.url, this.origin).href)}` : \"true\"}`,\n remaining,\n ),\n options.timeoutMs ?? 10_000,\n );\n });\n }\n\n async click(selector: string, timeoutMs = 10_000): Promise<void> {\n await this.step(`click ${selector}`, async () => {\n const deadline = Date.now() + timeoutMs;\n let previous: ClickBox | undefined;\n let reason = \"layout has not settled\";\n while (Date.now() < deadline) {\n const remaining = () => Math.max(1, deadline - Date.now());\n let box: ClickBox;\n try {\n box = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(25);\n continue;\n }\n reason = box.reason || \"target box is moving\";\n if (previous && boxesStable(previous, box)) {\n // Hover can move menus and overlays. Re-measure and hit-test after it.\n let hovered: ClickBox;\n try {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n { type: \"mouseMoved\", x: box.x, y: box.y },\n remaining(),\n );\n hovered = await this.evaluate<ClickBox>(\n `(${clickProbe.toString()})(${JSON.stringify(selector)})`,\n remaining(),\n );\n } catch (error) {\n reason = errorMessage(error);\n previous = undefined;\n await delay(Math.min(25, remaining()));\n continue;\n }\n if (Date.now() < deadline && boxesStable(box, hovered)) {\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mousePressed\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 1,\n clickCount: 1,\n },\n remaining(),\n );\n await this.cdp.command(\n \"Input.dispatchMouseEvent\",\n {\n type: \"mouseReleased\",\n x: hovered.x,\n y: hovered.y,\n button: \"left\",\n buttons: 0,\n clickCount: 1,\n },\n remaining(),\n );\n return;\n }\n reason = hovered.reason || \"hover changed the target box\";\n previous = undefined;\n } else previous = box;\n await delay(Math.min(50, remaining()));\n }\n throw new Error(`Cannot click ${JSON.stringify(selector)} within ${timeoutMs}ms: ${reason}`);\n });\n }\n\n async fill(selector: string, value: string): Promise<void> {\n await this.step(`fill ${selector}`, async () => {\n await this.click(selector);\n // The native value setter works with controlled inputs; input/change are\n // emitted after clearing. CDP insertText then performs actual text input.\n await this.evaluate(`(() => {\n const element=document.querySelector(${JSON.stringify(selector)});\n if (element !== document.activeElement) throw new Error('Fill target lost focus');\n if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled) {\n const prototype=element instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;\n const setter=Object.getOwnPropertyDescriptor(prototype,'value')?.set;\n if (!setter) throw new Error('Fill target has no native value setter');\n setter.call(element,'');\n } else if (element instanceof HTMLElement && element.isContentEditable) element.textContent='';\n else throw new Error('Fill target must be an editable input, textarea or contenteditable');\n element.dispatchEvent(new Event('input',{bubbles:true}));\n })()`);\n await this.evaluate(\n `document.activeElement === document.querySelector(${JSON.stringify(selector)}) || (() => { throw new Error(\"Fill target lost focus after input\"); })()`,\n );\n if (value) await this.cdp.command(\"Input.insertText\", { text: value });\n await this.evaluate(\n \"document.activeElement?.dispatchEvent(new Event('change',{bubbles:true}))\",\n );\n });\n }\n\n /** Appends Unicode text through the browser's text-input path. */\n async type(selector: string, text: string): Promise<void> {\n await this.step(`type ${selector}`, async () => {\n await this.click(selector);\n const needsEndKey = await this.evaluate<boolean>(\n `(() => { const element=document.querySelector(${JSON.stringify(selector)}); if (element !== document.activeElement || !((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && !element.readOnly && !element.disabled || element instanceof HTMLElement && element.isContentEditable)) throw new Error('Type target is not focused and editable'); if(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { if (typeof element.selectionStart !== \"number\") return true; element.setSelectionRange(element.value.length,element.value.length); } else { const range=document.createRange(); range.selectNodeContents(element); range.collapse(false); const selection=getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return false; })()`,\n );\n // Email/number inputs do not expose selectionRange; use their native\n // keyboard editing path instead of throwing on setSelectionRange.\n if (needsEndKey) {\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyDown\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n await this.cdp.command(\"Input.dispatchKeyEvent\", {\n type: \"keyUp\",\n key: \"End\",\n code: \"End\",\n windowsVirtualKeyCode: 35,\n });\n }\n await this.cdp.command(\"Input.insertText\", { text });\n });\n }\n\n async setViewport(viewport: Viewport): Promise<void> {\n if (\n !/^[a-z0-9_-]+$/i.test(viewport.name) ||\n !Number.isInteger(viewport.width) ||\n !Number.isInteger(viewport.height) ||\n viewport.width <= 0 ||\n viewport.height <= 0\n )\n throw new Error(\"Viewport requires a safe name and positive integer width/height\");\n await this.step(`viewport ${viewport.name} ${viewport.width}x${viewport.height}`, async () => {\n await this.cdp.command(\"Emulation.setDeviceMetricsOverride\", {\n width: viewport.width,\n height: viewport.height,\n deviceScaleFactor: 1,\n mobile: viewport.mobile ?? false,\n });\n this.viewport = { ...viewport };\n await this.evaluate(\n \"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve(true))))\",\n );\n });\n }\n\n async assertViewport(): Promise<ViewportReport> {\n return this.step(`assertViewport ${this.viewport.name}`, async () => {\n const report = await this.evaluate<ViewportReport>(`({\n requestedWidth:${this.viewport.width},innerWidth,documentWidth:document.documentElement.scrollWidth,bodyWidth:document.body?.scrollWidth ?? 0,\n offenders:[...document.querySelectorAll('*')].map(element=>{const box=element.getBoundingClientRect();return {tag:element.tagName,id:element.id,className:element.getAttribute('class')??'',width:box.width,scrollWidth:element.scrollWidth,right:box.right,left:box.left}})\n .filter(element=>element.right>${this.viewport.width} || element.left<0 || element.width>${this.viewport.width} || element.scrollWidth>${this.viewport.width})\n .sort((a,b)=>Math.max(b.width,b.scrollWidth,b.right)-Math.max(a.width,a.scrollWidth,a.right)).slice(0,10)\n })`);\n if (\n report.innerWidth !== report.requestedWidth ||\n report.documentWidth > report.requestedWidth ||\n report.bodyWidth > report.requestedWidth\n )\n throw new Error(\n `Horizontal overflow or expanded viewport at ${this.viewport.name}: ${JSON.stringify(report)}`,\n );\n return report;\n });\n }\n\n async screenshot(name: string): Promise<string> {\n if (!/^[a-z0-9_-]+$/i.test(name))\n throw new Error(\"Screenshot names must contain only letters, digits, '_' or '-'\");\n return this.step(`screenshot ${name}`, async () => {\n const file = `${String(this.evidence.screenshots.length + 1).padStart(3, \"0\")}-${name}-${this.viewport.name}.png`;\n const { data } = await this.cdp.command(\"Page.captureScreenshot\", { format: \"png\" });\n await writeFile(join(this.outputDirectory, file), Buffer.from(data, \"base64\"));\n this.evidence.screenshots.push({ name, file, viewport: { ...this.viewport } });\n return join(this.outputDirectory, file);\n });\n }\n\n async captureViewports(\n name: string,\n viewports = this.spec.viewports ?? defaultViewports,\n ): Promise<void> {\n const failures: unknown[] = [];\n const original = this.viewport;\n for (const viewport of viewports) {\n try {\n await this.setViewport(viewport);\n try {\n await this.assertViewport();\n } finally {\n await this.screenshot(name);\n }\n } catch (error) {\n failures.push(error);\n }\n }\n try {\n await this.setViewport(original);\n } catch (error) {\n failures.push(error);\n }\n if (failures.length) throw new AggregateError(failures, failures.map(errorMessage).join(\"; \"));\n }\n\n async waitForNetworkIdle(timeoutMs = 10_000): Promise<void> {\n await this.step(\"waitForNetworkIdle\", () =>\n this.poll(\n \"Network idle\",\n async () => {\n const pending = [...this.pendingRequests.values()].filter(\n (url) => !this.spec.allowPendingRequests?.some((pattern) => matches(pattern, url)),\n );\n if (pending.length) throw new Error(`Pending requests: ${pending.join(\", \")}`);\n return Date.now() - this.lastNetworkActivity >= 300;\n },\n timeoutMs,\n ),\n );\n }\n\n assertHealthy(): void {\n for (const entry of this.evidence.console)\n if (entry.level === \"error\")\n entry.allowed =\n this.spec.allowConsoleErrors?.some((pattern) => matches(pattern, entry.text)) ?? false;\n for (const entry of this.evidence.networkFailures)\n entry.allowed =\n this.spec.allowNetworkFailures?.some(\n (rule) =>\n matches(rule.url, entry.url) &&\n (rule.status === undefined || rule.status === entry.status) &&\n (rule.errorText === undefined || matches(rule.errorText, entry.errorText ?? \"\")),\n ) ?? false;\n const consoleErrors = this.evidence.console.filter(\n (entry) => entry.level === \"error\" && !entry.allowed,\n );\n const networkErrors = this.evidence.networkFailures.filter((entry) => !entry.allowed);\n if (consoleErrors.length || networkErrors.length)\n throw new Error(\n `Browser health failed: ${consoleErrors.length} console errors, ${networkErrors.length} failed HTTP/network requests\\n${JSON.stringify({ console: consoleErrors, network: networkErrors })}`,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { E2EEvidence, E2ESpec, RunE2EOptions } from "./types.mjs";
|
|
2
|
+
//#region ../../packages/core/src/e2e/run-e2e.d.ts
|
|
3
|
+
export type E2EResult = {
|
|
4
|
+
outputDirectory: string;
|
|
5
|
+
evidenceFile: string;
|
|
6
|
+
evidence: E2EEvidence;
|
|
7
|
+
};
|
|
8
|
+
export declare class E2ERunError extends Error {
|
|
9
|
+
readonly result: E2EResult;
|
|
10
|
+
constructor(result: E2EResult);
|
|
11
|
+
}
|
|
12
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
13
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
14
|
+
*/
|
|
15
|
+
export declare function runE2E(spec: E2ESpec, options?: RunE2EOptions): Promise<E2EResult>;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=run-e2e.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"mappings":";;YAOY;EAAc;EAAyB;EAAsB,UAAU;;qBACtE,oBAAoB;WACV,QAAQ;EAA7B,YAAqB,QAAQ;;;;;wBAWT,OAAO,MAAM,SAAS,UAAS,gBAAqB,QAAQ"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { AppFixture } from "./fixture.mjs";
|
|
2
|
+
import { OwnedBrowser } from "./browser.mjs";
|
|
3
|
+
import { errorMessage } from "./types.mjs";
|
|
4
|
+
import { BrowserPage } from "./page.mjs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
7
|
+
|
|
8
|
+
//#region ../../packages/core/src/e2e/run-e2e.ts
|
|
9
|
+
var E2ERunError = class extends Error {
|
|
10
|
+
constructor(result) {
|
|
11
|
+
super(`Browser e2e failed: ${result.evidence.errors.join("; ")}\nEvidence: ${result.evidenceFile}`);
|
|
12
|
+
this.result = result;
|
|
13
|
+
this.name = "E2ERunError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Each invocation owns a fresh app copy, data root, process groups, profile,
|
|
17
|
+
* screenshots and evidence file. Failures still publish evidence after cleanup.
|
|
18
|
+
*/
|
|
19
|
+
async function runE2E(spec, options = {}) {
|
|
20
|
+
if (!spec || !spec.app || typeof spec.run !== "function") throw new Error("An e2e spec must default-export defineE2E({ app, run })");
|
|
21
|
+
const base = resolve(options.outputDirectory ?? ".warlock/e2e");
|
|
22
|
+
await mkdir(base, { recursive: true });
|
|
23
|
+
const outputDirectory = await mkdtemp(join(base, "run-"));
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const evidence = {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28
|
+
durationMs: 0,
|
|
29
|
+
status: "running",
|
|
30
|
+
steps: [],
|
|
31
|
+
console: [],
|
|
32
|
+
networkFailures: [],
|
|
33
|
+
screenshots: [],
|
|
34
|
+
processes: [],
|
|
35
|
+
errors: []
|
|
36
|
+
};
|
|
37
|
+
const result = {
|
|
38
|
+
outputDirectory,
|
|
39
|
+
evidenceFile: join(outputDirectory, "evidence.json"),
|
|
40
|
+
evidence
|
|
41
|
+
};
|
|
42
|
+
let fixture;
|
|
43
|
+
let browser;
|
|
44
|
+
let page;
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const forwardAbort = () => abort.abort(options.signal?.reason ?? /* @__PURE__ */ new Error("Browser e2e aborted"));
|
|
47
|
+
options.signal?.addEventListener("abort", forwardAbort, { once: true });
|
|
48
|
+
if (options.signal?.aborted) forwardAbort();
|
|
49
|
+
const onInterrupt = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGINT)"));
|
|
50
|
+
const onTerminate = () => abort.abort(/* @__PURE__ */ new Error("Browser e2e interrupted (SIGTERM)"));
|
|
51
|
+
process.on("SIGINT", onInterrupt);
|
|
52
|
+
process.on("SIGTERM", onTerminate);
|
|
53
|
+
const timeout = setTimeout(() => abort.abort(/* @__PURE__ */ new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 12e4}ms`)), spec.timeoutMs ?? 12e4);
|
|
54
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
55
|
+
if (abort.signal.aborted) reject(abort.signal.reason);
|
|
56
|
+
else abort.signal.addEventListener("abort", () => reject(abort.signal.reason), { once: true });
|
|
57
|
+
});
|
|
58
|
+
aborted.catch(() => {});
|
|
59
|
+
try {
|
|
60
|
+
abort.signal.throwIfAborted();
|
|
61
|
+
fixture = await AppFixture.create(evidence);
|
|
62
|
+
const context = await fixture.start(spec.app, {
|
|
63
|
+
...options,
|
|
64
|
+
signal: abort.signal
|
|
65
|
+
});
|
|
66
|
+
abort.signal.throwIfAborted();
|
|
67
|
+
browser = new OwnedBrowser(fixture.root, evidence);
|
|
68
|
+
const connection = await browser.start({
|
|
69
|
+
...spec.browser,
|
|
70
|
+
binary: options.browser ?? spec.browser?.binary
|
|
71
|
+
}, abort.signal);
|
|
72
|
+
abort.signal.throwIfAborted();
|
|
73
|
+
page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);
|
|
74
|
+
await page.initialize();
|
|
75
|
+
const activePage = page;
|
|
76
|
+
await Promise.race([aborted, spec.run({
|
|
77
|
+
...context,
|
|
78
|
+
page: activePage,
|
|
79
|
+
evidence,
|
|
80
|
+
outputDirectory,
|
|
81
|
+
signal: abort.signal,
|
|
82
|
+
step: (name, action) => activePage.step(name, action)
|
|
83
|
+
})]);
|
|
84
|
+
await page.waitForNetworkIdle();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
evidence.errors.push(errorMessage(error));
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
if (page) {
|
|
90
|
+
try {
|
|
91
|
+
await page.captureViewports("final");
|
|
92
|
+
} catch (error) {
|
|
93
|
+
evidence.errors.push(errorMessage(error));
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await page.waitForNetworkIdle();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
evidence.errors.push(errorMessage(error));
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
page.assertHealthy();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
evidence.errors.push(errorMessage(error));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (fixture) for (const owned of fixture.processes) try {
|
|
107
|
+
await writeFile(join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`), owned.logs());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
evidence.errors.push(`Writing process log: ${errorMessage(error)}`);
|
|
110
|
+
}
|
|
111
|
+
if (browser?.process) try {
|
|
112
|
+
await writeFile(join(outputDirectory, `browser-${browser.process.record.pid}.log`), browser.process.logs());
|
|
113
|
+
} catch (error) {
|
|
114
|
+
evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await browser?.stop();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);
|
|
120
|
+
}
|
|
121
|
+
if (browser?.process && !browser.process.record.stopped) {
|
|
122
|
+
evidence.errors.push(`Fixture files retained at ${fixture?.root}: browser could not be safely stopped`);
|
|
123
|
+
const stopped = await Promise.allSettled(fixture?.processes.map((owned) => owned.stop()) ?? []);
|
|
124
|
+
for (const entry of stopped) if (entry.status === "rejected") evidence.errors.push(errorMessage(entry.reason));
|
|
125
|
+
} else try {
|
|
126
|
+
await fixture?.stop();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
evidence.errors.push(`App teardown: ${errorMessage(error)}`);
|
|
129
|
+
}
|
|
130
|
+
options.signal?.removeEventListener("abort", forwardAbort);
|
|
131
|
+
process.removeListener("SIGINT", onInterrupt);
|
|
132
|
+
process.removeListener("SIGTERM", onTerminate);
|
|
133
|
+
evidence.durationMs = Math.round(performance.now() - started);
|
|
134
|
+
evidence.status = evidence.errors.length ? "failed" : "passed";
|
|
135
|
+
await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + "\n");
|
|
136
|
+
}
|
|
137
|
+
if (evidence.status === "failed") throw new E2ERunError(result);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
export { E2ERunError, runE2E };
|
|
143
|
+
//# sourceMappingURL=run-e2e.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-e2e.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/run-e2e.ts"],"sourcesContent":["import { mkdir, mkdtemp, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { OwnedBrowser } from \"./browser\";\nimport { AppFixture } from \"./fixture\";\nimport { BrowserPage } from \"./page\";\nimport { errorMessage, type E2EEvidence, type E2ESpec, type RunE2EOptions } from \"./types\";\n\nexport type E2EResult = { outputDirectory: string; evidenceFile: string; evidence: E2EEvidence };\nexport class E2ERunError extends Error {\n constructor(readonly result: E2EResult) {\n super(\n `Browser e2e failed: ${result.evidence.errors.join(\"; \")}\\nEvidence: ${result.evidenceFile}`,\n );\n this.name = \"E2ERunError\";\n }\n}\n\n/** Each invocation owns a fresh app copy, data root, process groups, profile,\n * screenshots and evidence file. Failures still publish evidence after cleanup.\n */\nexport async function runE2E(spec: E2ESpec, options: RunE2EOptions = {}): Promise<E2EResult> {\n if (!spec || !spec.app || typeof spec.run !== \"function\")\n throw new Error(\"An e2e spec must default-export defineE2E({ app, run })\");\n const base = resolve(options.outputDirectory ?? \".warlock/e2e\");\n await mkdir(base, { recursive: true });\n const outputDirectory = await mkdtemp(join(base, \"run-\"));\n const started = performance.now();\n const evidence: E2EEvidence = {\n schemaVersion: 1,\n startedAt: new Date().toISOString(),\n durationMs: 0,\n status: \"running\",\n steps: [],\n console: [],\n networkFailures: [],\n screenshots: [],\n processes: [],\n errors: [],\n };\n const result: E2EResult = {\n outputDirectory,\n evidenceFile: join(outputDirectory, \"evidence.json\"),\n evidence,\n };\n let fixture: AppFixture | undefined;\n let browser: OwnedBrowser | undefined;\n let page: BrowserPage | undefined;\n const abort = new AbortController();\n const forwardAbort = () =>\n abort.abort(options.signal?.reason ?? new Error(\"Browser e2e aborted\"));\n options.signal?.addEventListener(\"abort\", forwardAbort, { once: true });\n if (options.signal?.aborted) forwardAbort();\n const onInterrupt = () => abort.abort(new Error(\"Browser e2e interrupted (SIGINT)\"));\n const onTerminate = () => abort.abort(new Error(\"Browser e2e interrupted (SIGTERM)\"));\n process.on(\"SIGINT\", onInterrupt);\n process.on(\"SIGTERM\", onTerminate);\n const timeout = setTimeout(\n () => abort.abort(new Error(`Browser e2e timed out after ${spec.timeoutMs ?? 120_000}ms`)),\n spec.timeoutMs ?? 120_000,\n );\n const aborted = new Promise<never>((_resolve, reject) => {\n if (abort.signal.aborted) reject(abort.signal.reason);\n else abort.signal.addEventListener(\"abort\", () => reject(abort.signal.reason), { once: true });\n });\n // Attach immediately: fixture startup may take time before the race is awaited.\n void aborted.catch(() => {});\n try {\n abort.signal.throwIfAborted();\n fixture = await AppFixture.create(evidence);\n const context = await fixture.start(spec.app, { ...options, signal: abort.signal });\n abort.signal.throwIfAborted();\n browser = new OwnedBrowser(fixture.root, evidence);\n const connection = await browser.start(\n { ...spec.browser, binary: options.browser ?? spec.browser?.binary },\n abort.signal,\n );\n abort.signal.throwIfAborted();\n page = new BrowserPage(connection, context.url, outputDirectory, evidence, spec);\n await page.initialize();\n const activePage = page;\n await Promise.race([\n aborted,\n spec.run({\n ...context,\n page: activePage,\n evidence,\n outputDirectory,\n signal: abort.signal,\n step: (name, action) => activePage.step(name, action),\n }),\n ]);\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n } finally {\n clearTimeout(timeout);\n // Capture both widths even when the spec failed. An overflow failure still\n // captures the offending layout and remains a run failure.\n if (page) {\n try {\n await page.captureViewports(\"final\");\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n await page.waitForNetworkIdle();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n try {\n page.assertHealthy();\n } catch (error) {\n evidence.errors.push(errorMessage(error));\n }\n }\n if (fixture) {\n for (const owned of fixture.processes) {\n try {\n await writeFile(\n join(outputDirectory, `${owned.record.role}-${owned.record.pid}.log`),\n owned.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing process log: ${errorMessage(error)}`);\n }\n }\n }\n if (browser?.process) {\n try {\n await writeFile(\n join(outputDirectory, `browser-${browser.process.record.pid}.log`),\n browser.process.logs(),\n );\n } catch (error) {\n evidence.errors.push(`Writing browser log: ${errorMessage(error)}`);\n }\n }\n try {\n await browser?.stop();\n } catch (error) {\n evidence.errors.push(`Browser teardown: ${errorMessage(error)}`);\n }\n // Never remove a live browser profile after an identity verification failure.\n if (browser?.process && !browser.process.record.stopped) {\n evidence.errors.push(\n `Fixture files retained at ${fixture?.root}: browser could not be safely stopped`,\n );\n const stopped = await Promise.allSettled(\n fixture?.processes.map((owned) => owned.stop()) ?? [],\n );\n for (const entry of stopped)\n if (entry.status === \"rejected\") evidence.errors.push(errorMessage(entry.reason));\n } else {\n try {\n await fixture?.stop();\n } catch (error) {\n evidence.errors.push(`App teardown: ${errorMessage(error)}`);\n }\n }\n options.signal?.removeEventListener(\"abort\", forwardAbort);\n process.removeListener(\"SIGINT\", onInterrupt);\n process.removeListener(\"SIGTERM\", onTerminate);\n evidence.durationMs = Math.round(performance.now() - started);\n evidence.status = evidence.errors.length ? \"failed\" : \"passed\";\n await writeFile(result.evidenceFile, JSON.stringify(evidence, null, 2) + \"\\n\");\n }\n if (evidence.status === \"failed\") throw new E2ERunError(result);\n return result;\n}\n"],"mappings":"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"}
|