@warlock.js/core 5.33.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 +26 -46
- package/esm/cli/commands/check/check-command.action.mjs +2 -1
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +2 -2
- 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/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.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 +1 -6
- 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 +0 -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/secrets/errors.d.mts +1 -1
- package/esm/secrets/errors.d.mts.map +1 -1
- package/esm/secrets/errors.mjs +2 -0
- package/esm/secrets/errors.mjs.map +1 -1
- package/esm/secrets/resolve-driver.mjs +14 -0
- package/esm/secrets/resolve-driver.mjs.map +1 -0
- package/esm/secrets/secret-store.d.mts.map +1 -1
- package/esm/secrets/secret-store.mjs +4 -1
- package/esm/secrets/secret-store.mjs.map +1 -1
- package/esm/secrets/secrets.d.mts.map +1 -1
- package/esm/secrets/secrets.mjs +10 -1
- package/esm/secrets/secrets.mjs.map +1 -1
- package/esm/secrets/types.d.mts +4 -0
- package/esm/secrets/types.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.d.mts +3 -3
- package/esm/tooling/check/lint-step.d.mts.map +1 -1
- package/esm/tooling/check/lint-step.mjs +3 -47
- package/esm/tooling/check/lint-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts +2 -2
- package/esm/tooling/check/run-check.mjs +4 -2
- 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 +2 -1
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/oxc-toolchain.mjs +59 -10
- package/esm/upgrade/codemods/oxc-toolchain.mjs.map +1 -1
- 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 +1 -1
- package/esm/upgrade/codemods/toolchain-templates.mjs.map +1 -1
- 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/warlock-config/types.d.mts +0 -1
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +243 -41
- package/llms.txt +2 -1
- package/package.json +19 -20
- package/skills/create-module/SKILL.md +2 -2
- package/skills/run-app/SKILL.md +3 -3
- package/skills/store-secrets/SKILL.md +40 -1
- package/skills/test-browser/SKILL.md +119 -0
- package/skills/upgrade-from-5-29/SKILL.md +70 -29
- package/skills/warlock-check/SKILL.md +3 -3
- package/skills/warlock-doctor/SKILL.md +2 -3
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs +0 -152
- package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +0 -1
- package/esm/dev-server/health-checker/eslint-support.mjs +0 -27
- package/esm/dev-server/health-checker/eslint-support.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 -183
- package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +0 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//#region ../../packages/core/src/e2e/cdp.d.ts
|
|
2
|
+
/** Wire shapes verified against the official protocol viewer and its source JSON:
|
|
3
|
+
* https://chromedevtools.github.io/devtools-protocol/
|
|
4
|
+
* https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
|
|
5
|
+
* Only the fields consumed by this harness are modeled here.
|
|
6
|
+
*/
|
|
7
|
+
type RemoteObject = {
|
|
8
|
+
value?: unknown;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
type ExceptionDetails = {
|
|
12
|
+
text: string;
|
|
13
|
+
exception?: RemoteObject;
|
|
14
|
+
};
|
|
15
|
+
export type CdpMethods = {
|
|
16
|
+
"Runtime.enable": {
|
|
17
|
+
params: Record<string, never>;
|
|
18
|
+
result: Record<string, never>;
|
|
19
|
+
};
|
|
20
|
+
"Runtime.evaluate": {
|
|
21
|
+
params: {
|
|
22
|
+
expression: string;
|
|
23
|
+
returnByValue: boolean;
|
|
24
|
+
awaitPromise: boolean;
|
|
25
|
+
silent: boolean;
|
|
26
|
+
};
|
|
27
|
+
result: {
|
|
28
|
+
result: RemoteObject;
|
|
29
|
+
exceptionDetails?: ExceptionDetails;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
"Page.enable": {
|
|
33
|
+
params: Record<string, never>;
|
|
34
|
+
result: Record<string, never>;
|
|
35
|
+
};
|
|
36
|
+
"Page.navigate": {
|
|
37
|
+
params: {
|
|
38
|
+
url: string;
|
|
39
|
+
};
|
|
40
|
+
result: {
|
|
41
|
+
frameId: string;
|
|
42
|
+
loaderId?: string;
|
|
43
|
+
errorText?: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
"Page.captureScreenshot": {
|
|
47
|
+
params: {
|
|
48
|
+
format: "png";
|
|
49
|
+
};
|
|
50
|
+
result: {
|
|
51
|
+
data: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
"Emulation.setDeviceMetricsOverride": {
|
|
55
|
+
params: {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
deviceScaleFactor: number;
|
|
59
|
+
mobile: boolean;
|
|
60
|
+
};
|
|
61
|
+
result: Record<string, never>;
|
|
62
|
+
};
|
|
63
|
+
"Input.dispatchMouseEvent": {
|
|
64
|
+
params: {
|
|
65
|
+
type: "mouseMoved" | "mousePressed" | "mouseReleased";
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
button?: "left";
|
|
69
|
+
buttons?: number;
|
|
70
|
+
clickCount?: number;
|
|
71
|
+
};
|
|
72
|
+
result: Record<string, never>;
|
|
73
|
+
};
|
|
74
|
+
"Input.insertText": {
|
|
75
|
+
params: {
|
|
76
|
+
text: string;
|
|
77
|
+
};
|
|
78
|
+
result: Record<string, never>;
|
|
79
|
+
};
|
|
80
|
+
"Input.dispatchKeyEvent": {
|
|
81
|
+
params: {
|
|
82
|
+
type: "keyDown" | "keyUp";
|
|
83
|
+
key: string;
|
|
84
|
+
code: string;
|
|
85
|
+
windowsVirtualKeyCode: number;
|
|
86
|
+
};
|
|
87
|
+
result: Record<string, never>;
|
|
88
|
+
};
|
|
89
|
+
"Network.enable": {
|
|
90
|
+
params: Record<string, never>;
|
|
91
|
+
result: Record<string, never>;
|
|
92
|
+
};
|
|
93
|
+
"Log.enable": {
|
|
94
|
+
params: Record<string, never>;
|
|
95
|
+
result: Record<string, never>;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
export type CdpEvents = {
|
|
99
|
+
"Page.loadEventFired": {
|
|
100
|
+
timestamp: number;
|
|
101
|
+
};
|
|
102
|
+
"Runtime.consoleAPICalled": {
|
|
103
|
+
type: string;
|
|
104
|
+
args: RemoteObject[];
|
|
105
|
+
};
|
|
106
|
+
"Runtime.exceptionThrown": {
|
|
107
|
+
exceptionDetails: ExceptionDetails;
|
|
108
|
+
};
|
|
109
|
+
"Log.entryAdded": {
|
|
110
|
+
entry: {
|
|
111
|
+
level: string;
|
|
112
|
+
text: string;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
"Network.requestWillBeSent": {
|
|
116
|
+
requestId: string;
|
|
117
|
+
request: {
|
|
118
|
+
url: string;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
"Network.responseReceived": {
|
|
122
|
+
requestId: string;
|
|
123
|
+
response: {
|
|
124
|
+
url: string;
|
|
125
|
+
status: number;
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
"Network.loadingFailed": {
|
|
129
|
+
requestId: string;
|
|
130
|
+
errorText: string;
|
|
131
|
+
canceled?: boolean;
|
|
132
|
+
};
|
|
133
|
+
"Network.loadingFinished": {
|
|
134
|
+
requestId: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
export declare class CdpConnection {
|
|
138
|
+
private socket;
|
|
139
|
+
private sequence;
|
|
140
|
+
private pending;
|
|
141
|
+
private listeners;
|
|
142
|
+
private closed;
|
|
143
|
+
private constructor();
|
|
144
|
+
static connect(url: string, timeoutMs?: number): Promise<CdpConnection>;
|
|
145
|
+
command<K extends keyof CdpMethods>(method: K, params: CdpMethods[K]["params"], timeoutMs?: number): Promise<CdpMethods[K]["result"]>;
|
|
146
|
+
on<K extends keyof CdpEvents>(method: K, listener: (params: CdpEvents[K]) => void): () => void;
|
|
147
|
+
private fail;
|
|
148
|
+
close(): void;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
export {};
|
|
152
|
+
//# sourceMappingURL=cdp.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"mappings":";;;;;;KAKK;EAAiB;EAAiB;;KAClC;EAAqB;EAAc,YAAY;;YACxC;EAEV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IACE;MAAU;MAAoB;MAAwB;MAAuB;;IAC7E;MAAU,QAAQ;MAAc,mBAAmB;;;EAGrD;IAAiB,QAAQ;IAAuB,QAAQ;;EAExD;IACE;MAAU;;IACV;MAAU;MAAiB;MAAmB;;;EAGhD;IAA4B;MAAU;;IAAiB;MAAU;;;EAEjE;IACE;MAAU;MAAe;MAAgB;MAA2B;;IACpE,QAAQ;;EAGV;IACE;MACE;MACA;MACA;MACA;MACA;MACA;;IAEF,QAAQ;;EAGV;IAAsB;MAAU;;IAAgB,QAAQ;;EAExD;IACE;MAAU;MAA2B;MAAa;MAAc;;IAChE,QAAQ;;EAGV;IAAoB,QAAQ;IAAuB,QAAQ;;EAE3D;IAAgB,QAAQ;IAAuB,QAAQ;;;YAE7C;EAEV;IAAyB;;EAEzB;IAA8B;IAAc,MAAM;;EAElD;IAA6B,kBAAkB;;EAE/C;IAAoB;MAAS;MAAe;;;EAE5C;IAA+B;IAAmB;MAAW;;;EAE7D;IAA8B;IAAmB;MAAY;MAAa;;;EAE1E;IAA2B;IAAmB;IAAmB;;EAEjE;IAA6B;;;qBAQlB;UAMiB;UALpB;UACA;UACA;UACA;UAED;SA4BM,QAAQ,aAAa,qBAAqB,QAAQ;EAiC/D,QAAQ,gBAAgB,YACtB,QAAQ,GACR,QAAQ,WAAW,cACnB,qBACC,QAAQ,WAAW;EA0BtB,GAAG,gBAAgB,WAAW,QAAQ,GAAG,WAAW,QAAQ,UAAU;UAU9D;EASR"}
|
package/esm/e2e/cdp.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region ../../packages/core/src/e2e/cdp.ts
|
|
2
|
+
var CdpConnection = class CdpConnection {
|
|
3
|
+
constructor(socket) {
|
|
4
|
+
this.socket = socket;
|
|
5
|
+
this.sequence = 0;
|
|
6
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
7
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
8
|
+
this.closed = false;
|
|
9
|
+
socket.addEventListener("message", ({ data }) => {
|
|
10
|
+
try {
|
|
11
|
+
const message = JSON.parse(String(data));
|
|
12
|
+
if (typeof message.id === "number") {
|
|
13
|
+
const pending = this.pending.get(message.id);
|
|
14
|
+
if (!pending) return;
|
|
15
|
+
clearTimeout(pending.timer);
|
|
16
|
+
this.pending.delete(message.id);
|
|
17
|
+
if (message.error) pending.reject(/* @__PURE__ */ new Error(`CDP: ${message.error.message}`));
|
|
18
|
+
else pending.resolve(message.result);
|
|
19
|
+
} else if (message.method) for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
this.fail(/* @__PURE__ */ new Error(`Invalid CDP message: ${String(error)}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
socket.addEventListener("close", () => this.fail(/* @__PURE__ */ new Error("CDP connection closed")));
|
|
25
|
+
socket.addEventListener("error", () => this.fail(/* @__PURE__ */ new Error("CDP WebSocket error")));
|
|
26
|
+
}
|
|
27
|
+
static async connect(url, timeoutMs = 1e4) {
|
|
28
|
+
if (typeof WebSocket !== "function") throw new Error("Browser e2e requires Node's global WebSocket (Node >=22.15, without --no-experimental-websocket)");
|
|
29
|
+
const socket = new WebSocket(url);
|
|
30
|
+
const connection = new CdpConnection(socket);
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
const cleanup = () => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
socket.removeEventListener("open", opened);
|
|
35
|
+
socket.removeEventListener("error", failed);
|
|
36
|
+
socket.removeEventListener("close", failed);
|
|
37
|
+
};
|
|
38
|
+
const opened = () => {
|
|
39
|
+
cleanup();
|
|
40
|
+
resolve();
|
|
41
|
+
};
|
|
42
|
+
const failed = () => {
|
|
43
|
+
cleanup();
|
|
44
|
+
socket.close();
|
|
45
|
+
reject(/* @__PURE__ */ new Error(`Cannot connect to owned browser CDP endpoint ${url}`));
|
|
46
|
+
};
|
|
47
|
+
const timer = setTimeout(failed, timeoutMs);
|
|
48
|
+
socket.addEventListener("open", opened, { once: true });
|
|
49
|
+
socket.addEventListener("error", failed, { once: true });
|
|
50
|
+
socket.addEventListener("close", failed, { once: true });
|
|
51
|
+
});
|
|
52
|
+
return connection;
|
|
53
|
+
}
|
|
54
|
+
command(method, params, timeoutMs = 1e4) {
|
|
55
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error(`CDP closed before ${method}`));
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const id = ++this.sequence;
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
this.pending.delete(id);
|
|
60
|
+
reject(/* @__PURE__ */ new Error(`CDP timeout after ${timeoutMs}ms: ${method}`));
|
|
61
|
+
}, Math.max(1, timeoutMs));
|
|
62
|
+
this.pending.set(id, {
|
|
63
|
+
resolve: (value) => resolve(value),
|
|
64
|
+
reject,
|
|
65
|
+
timer
|
|
66
|
+
});
|
|
67
|
+
try {
|
|
68
|
+
this.socket.send(JSON.stringify({
|
|
69
|
+
id,
|
|
70
|
+
method,
|
|
71
|
+
params
|
|
72
|
+
}));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
this.pending.delete(id);
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
on(method, listener) {
|
|
81
|
+
const wrapped = (params) => listener(params);
|
|
82
|
+
const listeners = this.listeners.get(method) ?? /* @__PURE__ */ new Set();
|
|
83
|
+
listeners.add(wrapped);
|
|
84
|
+
this.listeners.set(method, listeners);
|
|
85
|
+
return () => {
|
|
86
|
+
listeners.delete(wrapped);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
fail(error) {
|
|
90
|
+
this.closed = true;
|
|
91
|
+
for (const pending of this.pending.values()) {
|
|
92
|
+
clearTimeout(pending.timer);
|
|
93
|
+
pending.reject(error);
|
|
94
|
+
}
|
|
95
|
+
this.pending.clear();
|
|
96
|
+
}
|
|
97
|
+
close() {
|
|
98
|
+
this.fail(/* @__PURE__ */ new Error("CDP connection closed by harness"));
|
|
99
|
+
this.listeners.clear();
|
|
100
|
+
this.socket.close();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
export { CdpConnection };
|
|
106
|
+
//# sourceMappingURL=cdp.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp.mjs","names":[],"sources":["../../../../../../../../packages/core/src/e2e/cdp.ts"],"sourcesContent":["/** Wire shapes verified against the official protocol viewer and its source JSON:\n * https://chromedevtools.github.io/devtools-protocol/\n * https://github.com/ChromeDevTools/devtools-protocol/tree/master/json\n * Only the fields consumed by this harness are modeled here.\n */\ntype RemoteObject = { value?: unknown; description?: string };\ntype ExceptionDetails = { text: string; exception?: RemoteObject };\nexport type CdpMethods = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-enable\n \"Runtime.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#method-evaluate\n \"Runtime.evaluate\": {\n params: { expression: string; returnByValue: boolean; awaitPromise: boolean; silent: boolean };\n result: { result: RemoteObject; exceptionDetails?: ExceptionDetails };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-enable\n \"Page.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-navigate\n \"Page.navigate\": {\n params: { url: string };\n result: { frameId: string; loaderId?: string; errorText?: string };\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot\n \"Page.captureScreenshot\": { params: { format: \"png\" }; result: { data: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Emulation/#method-setDeviceMetricsOverride\n \"Emulation.setDeviceMetricsOverride\": {\n params: { width: number; height: number; deviceScaleFactor: number; mobile: boolean };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent\n \"Input.dispatchMouseEvent\": {\n params: {\n type: \"mouseMoved\" | \"mousePressed\" | \"mouseReleased\";\n x: number;\n y: number;\n button?: \"left\";\n buttons?: number;\n clickCount?: number;\n };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText\n \"Input.insertText\": { params: { text: string }; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent\n \"Input.dispatchKeyEvent\": {\n params: { type: \"keyDown\" | \"keyUp\"; key: string; code: string; windowsVirtualKeyCode: number };\n result: Record<string, never>;\n };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-enable\n \"Network.enable\": { params: Record<string, never>; result: Record<string, never> };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#method-enable\n \"Log.enable\": { params: Record<string, never>; result: Record<string, never> };\n};\nexport type CdpEvents = {\n // https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-loadEventFired\n \"Page.loadEventFired\": { timestamp: number };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-consoleAPICalled\n \"Runtime.consoleAPICalled\": { type: string; args: RemoteObject[] };\n // https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#event-exceptionThrown\n \"Runtime.exceptionThrown\": { exceptionDetails: ExceptionDetails };\n // https://chromedevtools.github.io/devtools-protocol/tot/Log/#event-entryAdded\n \"Log.entryAdded\": { entry: { level: string; text: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-requestWillBeSent\n \"Network.requestWillBeSent\": { requestId: string; request: { url: string } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-responseReceived\n \"Network.responseReceived\": { requestId: string; response: { url: string; status: number } };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFailed\n \"Network.loadingFailed\": { requestId: string; errorText: string; canceled?: boolean };\n // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-loadingFinished\n \"Network.loadingFinished\": { requestId: string };\n};\ntype Pending = {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport class CdpConnection {\n private sequence = 0;\n private pending = new Map<number, Pending>();\n private listeners = new Map<string, Set<(params: unknown) => void>>();\n private closed = false;\n\n private constructor(private socket: WebSocket) {\n socket.addEventListener(\"message\", ({ data }) => {\n try {\n const message = JSON.parse(String(data)) as {\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: { message: string };\n };\n if (typeof message.id === \"number\") {\n const pending = this.pending.get(message.id);\n if (!pending) return;\n clearTimeout(pending.timer);\n this.pending.delete(message.id);\n if (message.error) pending.reject(new Error(`CDP: ${message.error.message}`));\n else pending.resolve(message.result);\n } else if (message.method) {\n for (const listener of this.listeners.get(message.method) ?? []) listener(message.params);\n }\n } catch (error) {\n this.fail(new Error(`Invalid CDP message: ${String(error)}`));\n }\n });\n socket.addEventListener(\"close\", () => this.fail(new Error(\"CDP connection closed\")));\n socket.addEventListener(\"error\", () => this.fail(new Error(\"CDP WebSocket error\")));\n }\n\n static async connect(url: string, timeoutMs = 10_000): Promise<CdpConnection> {\n // Stable since Node 22.4, present in the minimum supported Node 22.15.\n // https://nodejs.org/download/release/v22.15.0/docs/api/globals.html#websocket\n if (typeof WebSocket !== \"function\")\n throw new Error(\n \"Browser e2e requires Node's global WebSocket (Node >=22.15, without --no-experimental-websocket)\",\n );\n const socket = new WebSocket(url);\n const connection = new CdpConnection(socket);\n await new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n clearTimeout(timer);\n socket.removeEventListener(\"open\", opened);\n socket.removeEventListener(\"error\", failed);\n socket.removeEventListener(\"close\", failed);\n };\n const opened = () => {\n cleanup();\n resolve();\n };\n const failed = () => {\n cleanup();\n socket.close();\n reject(new Error(`Cannot connect to owned browser CDP endpoint ${url}`));\n };\n const timer = setTimeout(failed, timeoutMs);\n socket.addEventListener(\"open\", opened, { once: true });\n socket.addEventListener(\"error\", failed, { once: true });\n socket.addEventListener(\"close\", failed, { once: true });\n });\n return connection;\n }\n\n command<K extends keyof CdpMethods>(\n method: K,\n params: CdpMethods[K][\"params\"],\n timeoutMs = 10_000,\n ): Promise<CdpMethods[K][\"result\"]> {\n if (this.closed) return Promise.reject(new Error(`CDP closed before ${method}`));\n return new Promise<CdpMethods[K][\"result\"]>((resolve, reject) => {\n const id = ++this.sequence;\n const timer = setTimeout(\n () => {\n this.pending.delete(id);\n reject(new Error(`CDP timeout after ${timeoutMs}ms: ${method}`));\n },\n Math.max(1, timeoutMs),\n );\n this.pending.set(id, {\n resolve: (value) => resolve(value as CdpMethods[K][\"result\"]),\n reject,\n timer,\n });\n try {\n this.socket.send(JSON.stringify({ id, method, params }));\n } catch (error) {\n clearTimeout(timer);\n this.pending.delete(id);\n reject(error);\n }\n });\n }\n\n on<K extends keyof CdpEvents>(method: K, listener: (params: CdpEvents[K]) => void): () => void {\n const wrapped = (params: unknown) => listener(params as CdpEvents[K]);\n const listeners = this.listeners.get(method) ?? new Set();\n listeners.add(wrapped);\n this.listeners.set(method, listeners);\n return () => {\n listeners.delete(wrapped);\n };\n }\n\n private fail(error: Error): void {\n this.closed = true;\n for (const pending of this.pending.values()) {\n clearTimeout(pending.timer);\n pending.reject(error);\n }\n this.pending.clear();\n }\n\n close(): void {\n this.fail(new Error(\"CDP connection closed by harness\"));\n this.listeners.clear();\n this.socket.close();\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { OwnedProcess } from "./owned-process.mjs";
|
|
2
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { cp, lstat, mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { setTimeout } from "node:timers/promises";
|
|
7
|
+
|
|
8
|
+
//#region ../../packages/core/src/e2e/fixture.ts
|
|
9
|
+
/** Deliberately does not inherit app secrets, NODE_OPTIONS, or service endpoints. */
|
|
10
|
+
function isolatedEnvironment(directory, additions, paths = []) {
|
|
11
|
+
const env = {};
|
|
12
|
+
for (const key of [
|
|
13
|
+
"PATH",
|
|
14
|
+
"SystemRoot",
|
|
15
|
+
"WINDIR",
|
|
16
|
+
"COMSPEC",
|
|
17
|
+
"PATHEXT",
|
|
18
|
+
"LANG",
|
|
19
|
+
"LC_ALL",
|
|
20
|
+
"TZ"
|
|
21
|
+
]) if (process.env[key]) env[key] = process.env[key];
|
|
22
|
+
return {
|
|
23
|
+
...env,
|
|
24
|
+
...additions,
|
|
25
|
+
HOME: join(directory, "home"),
|
|
26
|
+
USERPROFILE: join(directory, "home"),
|
|
27
|
+
XDG_CONFIG_HOME: join(directory, "config"),
|
|
28
|
+
XDG_CACHE_HOME: join(directory, "cache"),
|
|
29
|
+
TMPDIR: join(directory, "tmp"),
|
|
30
|
+
TEMP: join(directory, "tmp"),
|
|
31
|
+
TMP: join(directory, "tmp"),
|
|
32
|
+
PATH: [...paths, env.PATH ?? ""].join(delimiter)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function reservePort() {
|
|
36
|
+
const server = createServer();
|
|
37
|
+
await new Promise((accept, reject) => {
|
|
38
|
+
server.once("error", reject);
|
|
39
|
+
server.listen(0, "127.0.0.1", accept);
|
|
40
|
+
});
|
|
41
|
+
const address = server.address();
|
|
42
|
+
if (!address || typeof address === "string") throw new Error("Could not reserve an ephemeral fixture port");
|
|
43
|
+
return {
|
|
44
|
+
port: address.port,
|
|
45
|
+
release: () => new Promise((accept, reject) => server.close((error) => error ? reject(error) : accept()))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function linkDependencies(source, target) {
|
|
49
|
+
let current = source;
|
|
50
|
+
while (true) {
|
|
51
|
+
const modules = join(current, "node_modules");
|
|
52
|
+
try {
|
|
53
|
+
if ((await stat(modules)).isDirectory()) {
|
|
54
|
+
await symlink(await realpath(modules), join(target, "node_modules"), process.platform === "win32" ? "junction" : "dir");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
59
|
+
}
|
|
60
|
+
const parent = dirname(current);
|
|
61
|
+
if (parent === current) return;
|
|
62
|
+
current = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var AppFixture = class AppFixture {
|
|
66
|
+
constructor(root, evidence) {
|
|
67
|
+
this.root = root;
|
|
68
|
+
this.evidence = evidence;
|
|
69
|
+
this.processes = [];
|
|
70
|
+
}
|
|
71
|
+
static async create(evidence) {
|
|
72
|
+
return new AppFixture(await mkdtemp(join(tmpdir(), "warlock-e2e-")), evidence);
|
|
73
|
+
}
|
|
74
|
+
async start(options, runOptions) {
|
|
75
|
+
runOptions.signal?.throwIfAborted();
|
|
76
|
+
const source = await realpath(resolve(options.directory ?? process.cwd()));
|
|
77
|
+
const directory = join(this.root, "app");
|
|
78
|
+
const dataRoot = join(this.root, "data");
|
|
79
|
+
const portFile = join(this.root, "port");
|
|
80
|
+
const output = relative(source, resolve(runOptions.outputDirectory ?? ".warlock/e2e"));
|
|
81
|
+
const excluded = [
|
|
82
|
+
"node_modules",
|
|
83
|
+
".git",
|
|
84
|
+
".warlock",
|
|
85
|
+
...output && !output.startsWith("..") && !isAbsolute(output) ? [output] : [],
|
|
86
|
+
...options.exclude ?? []
|
|
87
|
+
];
|
|
88
|
+
if (excluded.some((entry) => isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))) throw new Error("Fixture exclusions must be relative paths without '..'");
|
|
89
|
+
await cp(source, directory, {
|
|
90
|
+
recursive: true,
|
|
91
|
+
dereference: false,
|
|
92
|
+
filter: async (file) => {
|
|
93
|
+
const path = relative(source, file);
|
|
94
|
+
if (!path) return true;
|
|
95
|
+
if (file === this.root || this.root.startsWith(`${file}${sep}`)) return false;
|
|
96
|
+
const segments = path.split(sep);
|
|
97
|
+
if (segments.some((entry) => entry === ".env" || entry.startsWith(".env."))) return false;
|
|
98
|
+
if (excluded.some((entry) => path === entry || path.startsWith(`${entry}${sep}`) || entry === "node_modules" && segments.includes(entry))) return false;
|
|
99
|
+
return !(await lstat(file).catch(() => void 0))?.isSymbolicLink();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
await linkDependencies(source, directory);
|
|
103
|
+
for (const path of [
|
|
104
|
+
dataRoot,
|
|
105
|
+
"home",
|
|
106
|
+
"config",
|
|
107
|
+
"cache",
|
|
108
|
+
"tmp"
|
|
109
|
+
].map((path) => isAbsolute(path) ? path : join(this.root, path))) await mkdir(path, { recursive: true });
|
|
110
|
+
const reservation = await reservePort();
|
|
111
|
+
const context = {
|
|
112
|
+
directory,
|
|
113
|
+
dataRoot,
|
|
114
|
+
port: reservation.port,
|
|
115
|
+
url: `http://127.0.0.1:${reservation.port}`,
|
|
116
|
+
portFile
|
|
117
|
+
};
|
|
118
|
+
this.context = context;
|
|
119
|
+
try {
|
|
120
|
+
await options.prepare?.(context);
|
|
121
|
+
runOptions.signal?.throwIfAborted();
|
|
122
|
+
const additions = typeof options.env === "function" ? options.env(context) : options.env ?? {};
|
|
123
|
+
const env = isolatedEnvironment(this.root, {
|
|
124
|
+
NODE_ENV: "production",
|
|
125
|
+
...additions,
|
|
126
|
+
HOST: "127.0.0.1",
|
|
127
|
+
HTTP_HOST: "127.0.0.1",
|
|
128
|
+
PORT: String(options.portFromFile ? 0 : context.port),
|
|
129
|
+
HTTP_PORT: String(options.portFromFile ? 0 : context.port),
|
|
130
|
+
WARLOCK_E2E_DATA_ROOT: dataRoot,
|
|
131
|
+
WARLOCK_E2E_PORT_FILE: portFile
|
|
132
|
+
}, [...runOptions.path ?? [], ...options.path ?? []].map((path) => resolve(directory, path)));
|
|
133
|
+
await writeFile(join(directory, ".env"), Object.entries(env).map(([key, value]) => `${key}=${JSON.stringify(value ?? "")}`).join("\n") + "\n", { mode: 384 });
|
|
134
|
+
const timeoutMs = options.startupTimeoutMs ?? 3e4;
|
|
135
|
+
if (options.build) {
|
|
136
|
+
const command = typeof options.build === "function" ? options.build(context) : options.build;
|
|
137
|
+
const build = await OwnedProcess.start("build", command, directory, env, this.root, this.evidence.processes, (owned) => this.processes.push(owned));
|
|
138
|
+
await build.wait(timeoutMs, runOptions.signal);
|
|
139
|
+
await build.stop();
|
|
140
|
+
}
|
|
141
|
+
await reservation.release();
|
|
142
|
+
runOptions.signal?.throwIfAborted();
|
|
143
|
+
const command = typeof options.start === "function" ? options.start(context) : options.start ?? [process.execPath, "dist/app.js"];
|
|
144
|
+
const app = await OwnedProcess.start("app", command, directory, env, this.root, this.evidence.processes, (owned) => this.processes.push(owned));
|
|
145
|
+
const deadline = Date.now() + timeoutMs;
|
|
146
|
+
let reason = "No successful readiness response";
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
runOptions.signal?.throwIfAborted();
|
|
149
|
+
app.assertRunning();
|
|
150
|
+
try {
|
|
151
|
+
if (options.portFromFile) {
|
|
152
|
+
const port = Number((await readFile(portFile, "utf8")).trim());
|
|
153
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new Error("Port file must contain a bound TCP port between 1 and 65535");
|
|
154
|
+
context.port = port;
|
|
155
|
+
context.url = `http://127.0.0.1:${port}`;
|
|
156
|
+
}
|
|
157
|
+
const ready = new URL(options.readyPath ?? "/", context.url);
|
|
158
|
+
if (ready.origin !== context.url) throw new Error("Readiness URL must stay on the fixture origin");
|
|
159
|
+
const response = await fetch(ready, {
|
|
160
|
+
redirect: "manual",
|
|
161
|
+
signal: AbortSignal.timeout(Math.min(1e3, Math.max(1, deadline - Date.now())))
|
|
162
|
+
});
|
|
163
|
+
await response.body?.cancel();
|
|
164
|
+
if (response.status >= 200 && response.status < 400) {
|
|
165
|
+
await setTimeout(100);
|
|
166
|
+
app.assertRunning();
|
|
167
|
+
this.evidence.url = context.url;
|
|
168
|
+
return context;
|
|
169
|
+
}
|
|
170
|
+
reason = `Readiness HTTP ${response.status}`;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
reason = String(error);
|
|
173
|
+
}
|
|
174
|
+
await setTimeout(50);
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`App readiness timed out: ${reason}\n${app.logs().slice(-8e3)}`);
|
|
177
|
+
} finally {
|
|
178
|
+
await reservation.release().catch(() => {});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async stop() {
|
|
182
|
+
const errors = (await Promise.allSettled(this.processes.map((process) => process.stop()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
183
|
+
if (errors.length) throw new AggregateError(errors, "Owned fixture teardown failed; disposable files retained for inspection: " + errors.map(String).join("; "));
|
|
184
|
+
await rm(this.root, {
|
|
185
|
+
recursive: true,
|
|
186
|
+
force: true,
|
|
187
|
+
maxRetries: 8,
|
|
188
|
+
retryDelay: 100
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
export { AppFixture, isolatedEnvironment };
|
|
195
|
+
//# sourceMappingURL=fixture.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.mjs","names":["delay"],"sources":["../../../../../../../../packages/core/src/e2e/fixture.ts"],"sourcesContent":["import {\n cp,\n mkdir,\n mkdtemp,\n readFile,\n realpath,\n rm,\n lstat,\n stat,\n symlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { createServer } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { OwnedProcess } from \"./owned-process\";\nimport type {\n AppFixtureOptions,\n Command,\n E2EEvidence,\n FixtureContext,\n RunE2EOptions,\n} from \"./types\";\n\n/** Deliberately does not inherit app secrets, NODE_OPTIONS, or service endpoints. */\nexport function isolatedEnvironment(\n directory: string,\n additions: Record<string, string>,\n paths: string[] = [],\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n for (const key of [\"PATH\", \"SystemRoot\", \"WINDIR\", \"COMSPEC\", \"PATHEXT\", \"LANG\", \"LC_ALL\", \"TZ\"])\n if (process.env[key]) env[key] = process.env[key];\n return {\n ...env,\n ...additions,\n HOME: join(directory, \"home\"),\n USERPROFILE: join(directory, \"home\"),\n XDG_CONFIG_HOME: join(directory, \"config\"),\n XDG_CACHE_HOME: join(directory, \"cache\"),\n TMPDIR: join(directory, \"tmp\"),\n TEMP: join(directory, \"tmp\"),\n TMP: join(directory, \"tmp\"),\n PATH: [...paths, env.PATH ?? \"\"].join(delimiter),\n };\n}\n\nasync function reservePort(): Promise<{ port: number; release: () => Promise<void> }> {\n const server = createServer();\n await new Promise<void>((accept, reject) => {\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", accept);\n });\n const address = server.address();\n if (!address || typeof address === \"string\")\n throw new Error(\"Could not reserve an ephemeral fixture port\");\n return {\n port: address.port,\n release: () =>\n new Promise<void>((accept, reject) =>\n server.close((error) => (error ? reject(error) : accept())),\n ),\n };\n}\n\nasync function linkDependencies(source: string, target: string): Promise<void> {\n // A workspace app may resolve dependencies from a parent node_modules.\n let current = source;\n while (true) {\n const modules = join(current, \"node_modules\");\n try {\n if ((await stat(modules)).isDirectory()) {\n await symlink(\n await realpath(modules),\n join(target, \"node_modules\"),\n process.platform === \"win32\" ? \"junction\" : \"dir\",\n );\n return;\n }\n } catch (error) {\n if (!(error instanceof Error && \"code\" in error && error.code === \"ENOENT\")) throw error;\n }\n const parent = dirname(current);\n if (parent === current) return;\n current = parent;\n }\n}\n\nexport class AppFixture {\n readonly processes: OwnedProcess[] = [];\n context?: FixtureContext;\n private constructor(\n readonly root: string,\n private evidence: E2EEvidence,\n ) {}\n\n static async create(evidence: E2EEvidence): Promise<AppFixture> {\n return new AppFixture(await mkdtemp(join(tmpdir(), \"warlock-e2e-\")), evidence);\n }\n\n async start(options: AppFixtureOptions, runOptions: RunE2EOptions): Promise<FixtureContext> {\n runOptions.signal?.throwIfAborted();\n const source = await realpath(resolve(options.directory ?? process.cwd()));\n const directory = join(this.root, \"app\");\n const dataRoot = join(this.root, \"data\");\n const portFile = join(this.root, \"port\");\n const output = relative(source, resolve(runOptions.outputDirectory ?? \".warlock/e2e\"));\n const excluded = [\n \"node_modules\",\n \".git\",\n \".warlock\",\n ...(output && !output.startsWith(\"..\") && !isAbsolute(output) ? [output] : []),\n ...(options.exclude ?? []),\n ];\n if (excluded.some((entry) => isAbsolute(entry) || entry.split(/[\\\\/]/).includes(\"..\")))\n throw new Error(\"Fixture exclusions must be relative paths without '..'\");\n await cp(source, directory, {\n recursive: true,\n // Never copy dotenv files or follow symlinks into shared app data.\n dereference: false,\n filter: async (file) => {\n const path = relative(source, file);\n if (!path) return true;\n if (file === this.root || this.root.startsWith(`${file}${sep}`)) return false;\n const segments = path.split(sep);\n if (segments.some((entry) => entry === \".env\" || entry.startsWith(\".env.\"))) return false;\n if (\n excluded.some(\n (entry) =>\n path === entry ||\n path.startsWith(`${entry}${sep}`) ||\n (entry === \"node_modules\" && segments.includes(entry)),\n )\n )\n return false;\n return !(await lstat(file).catch(() => undefined))?.isSymbolicLink();\n },\n });\n // cp preserves links by default; reject app links to keep writes disposable.\n // Dependencies are the sole explicit read-only-by-contract shared link.\n await linkDependencies(source, directory);\n for (const path of [dataRoot, \"home\", \"config\", \"cache\", \"tmp\"].map((path) =>\n isAbsolute(path) ? path : join(this.root, path),\n ))\n await mkdir(path, { recursive: true });\n const reservation = await reservePort();\n const context: FixtureContext = {\n directory,\n dataRoot,\n port: reservation.port,\n url: `http://127.0.0.1:${reservation.port}`,\n portFile,\n };\n this.context = context;\n try {\n await options.prepare?.(context);\n runOptions.signal?.throwIfAborted();\n const additions =\n typeof options.env === \"function\" ? options.env(context) : (options.env ?? {});\n const env = isolatedEnvironment(\n this.root,\n {\n NODE_ENV: \"production\",\n ...additions,\n HOST: \"127.0.0.1\",\n HTTP_HOST: \"127.0.0.1\",\n PORT: String(options.portFromFile ? 0 : context.port),\n HTTP_PORT: String(options.portFromFile ? 0 : context.port),\n WARLOCK_E2E_DATA_ROOT: dataRoot,\n WARLOCK_E2E_PORT_FILE: portFile,\n },\n [...(runOptions.path ?? []), ...(options.path ?? [])].map((path) =>\n resolve(directory, path),\n ),\n );\n // A framework env() consumer sees the same owned variables as process.env.\n await writeFile(\n join(directory, \".env\"),\n Object.entries(env)\n .map(([key, value]) => `${key}=${JSON.stringify(value ?? \"\")}`)\n .join(\"\\n\") + \"\\n\",\n { mode: 0o600 },\n );\n const timeoutMs = options.startupTimeoutMs ?? 30_000;\n if (options.build) {\n const command =\n typeof options.build === \"function\" ? options.build(context) : options.build;\n const build = await OwnedProcess.start(\n \"build\",\n command,\n directory,\n env,\n this.root,\n this.evidence.processes,\n (owned) => this.processes.push(owned),\n );\n await build.wait(timeoutMs, runOptions.signal);\n await build.stop();\n }\n await reservation.release();\n runOptions.signal?.throwIfAborted();\n const command: Command =\n typeof options.start === \"function\"\n ? options.start(context)\n : (options.start ?? [process.execPath, \"dist/app.js\"]);\n const app = await OwnedProcess.start(\n \"app\",\n command,\n directory,\n env,\n this.root,\n this.evidence.processes,\n (owned) => this.processes.push(owned),\n );\n const deadline = Date.now() + timeoutMs;\n let reason = \"No successful readiness response\";\n while (Date.now() < deadline) {\n runOptions.signal?.throwIfAborted();\n app.assertRunning();\n try {\n if (options.portFromFile) {\n const port = Number((await readFile(portFile, \"utf8\")).trim());\n if (!Number.isInteger(port) || port <= 0 || port > 65535)\n throw new Error(\"Port file must contain a bound TCP port between 1 and 65535\");\n context.port = port;\n context.url = `http://127.0.0.1:${port}`;\n }\n const ready = new URL(options.readyPath ?? \"/\", context.url);\n if (ready.origin !== context.url)\n throw new Error(\"Readiness URL must stay on the fixture origin\");\n const response = await fetch(ready, {\n redirect: \"manual\",\n signal: AbortSignal.timeout(Math.min(1000, Math.max(1, deadline - Date.now()))),\n });\n await response.body?.cancel();\n if (response.status >= 200 && response.status < 400) {\n // Give foreground startup failures a chance to arrive over IPC.\n await delay(100);\n app.assertRunning();\n this.evidence.url = context.url;\n return context;\n }\n reason = `Readiness HTTP ${response.status}`;\n } catch (error) {\n reason = String(error);\n }\n await delay(50);\n }\n throw new Error(`App readiness timed out: ${reason}\\n${app.logs().slice(-8000)}`);\n } finally {\n await reservation.release().catch(() => {});\n }\n }\n\n async stop(): Promise<void> {\n const results = await Promise.allSettled(this.processes.map((process) => process.stop()));\n const errors = results.flatMap((result) =>\n result.status === \"rejected\" ? [result.reason] : [],\n );\n if (errors.length)\n throw new AggregateError(\n errors,\n \"Owned fixture teardown failed; disposable files retained for inspection: \" +\n errors.map(String).join(\"; \"),\n );\n await rm(this.root, { recursive: true, force: true, maxRetries: 8, retryDelay: 100 });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { BrowserPage } from "./page.mjs";
|
|
2
|
+
import { AppFixtureOptions, BrowserOptions, Command, ConsoleEntry, E2EContext, E2EEvidence, E2ESpec, FixtureContext, NetworkAllowance, NetworkFailure, Pattern, ProcessEvidence, RunE2EOptions, StepEvidence, Viewport, defaultViewports, defineE2E } from "./types.mjs";
|
|
3
|
+
import { findBrowserBinary } from "./browser.mjs";
|
|
4
|
+
import { E2EResult, E2ERunError, runE2E } from "./run-e2e.mjs";
|
|
5
|
+
export { type AppFixtureOptions, type BrowserOptions, BrowserPage, type Command, type ConsoleEntry, type E2EContext, type E2EEvidence, type E2EResult, E2ERunError, type E2ESpec, type FixtureContext, type NetworkAllowance, type NetworkFailure, type Pattern, type ProcessEvidence, type RunE2EOptions, type StepEvidence, type Viewport, defaultViewports, defineE2E, findBrowserBinary, runE2E };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { findBrowserBinary } from "./browser.mjs";
|
|
2
|
+
import { defaultViewports, defineE2E } from "./types.mjs";
|
|
3
|
+
import { BrowserPage } from "./page.mjs";
|
|
4
|
+
import { E2ERunError, runE2E } from "./run-e2e.mjs";
|
|
5
|
+
|
|
6
|
+
export { BrowserPage, E2ERunError, defaultViewports, defineE2E, findBrowserBinary, runE2E };
|