@mochi.js/core 0.3.0 → 0.6.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/README.md +19 -10
- package/package.json +4 -4
- package/src/__tests__/cookies-jar.test.ts +361 -0
- package/src/__tests__/default-profile.test.ts +181 -0
- package/src/__tests__/dx-cluster.e2e.test.ts +245 -0
- package/src/__tests__/init-injector.e2e.test.ts +144 -0
- package/src/__tests__/init-injector.test.ts +249 -0
- package/src/__tests__/inject.test.ts +80 -164
- package/src/__tests__/page-dx-cluster.test.ts +292 -0
- package/src/__tests__/proc-linux-server.test.ts +243 -0
- package/src/__tests__/proxy-auth.test.ts +22 -55
- package/src/__tests__/screenshot.e2e.test.ts +126 -0
- package/src/__tests__/screenshot.test.ts +363 -0
- package/src/cdp/init-injector.ts +644 -0
- package/src/default-profile.ts +112 -0
- package/src/index.ts +33 -1
- package/src/launch.ts +199 -10
- package/src/linux-server.ts +157 -0
- package/src/page.ts +410 -8
- package/src/proc.ts +48 -5
- package/src/proxy-auth.ts +26 -107
- package/src/session.ts +367 -68
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Linux-server environment auto-detection that drives
|
|
3
|
+
* `LaunchOptions.headlessMode` defaulting (task 0258).
|
|
4
|
+
*
|
|
5
|
+
* Two layers under test:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link detectLinuxServerEnv} — the pure classifier. Stub
|
|
8
|
+
* `(platform, env, uid, container probes)` and assert the
|
|
9
|
+
* `LinuxServerEnv` summary it returns.
|
|
10
|
+
* 2. {@link resolveHeadlessMode} — the resolution table that maps
|
|
11
|
+
* `(LaunchOptions, LinuxServerEnv)` → `"new" | "legacy" | "off"`. Order
|
|
12
|
+
* of precedence is load-bearing for the docs we ship; tests pin it.
|
|
13
|
+
*
|
|
14
|
+
* We DO NOT spawn Chromium here — the goal is to lock the decisions against
|
|
15
|
+
* regressions without taking the cost of a real launch. The flag-emit
|
|
16
|
+
* behaviour for the resolved mode is covered separately in
|
|
17
|
+
* `proc.test.ts` ("appends --headless=new when headless is true" et al.).
|
|
18
|
+
*
|
|
19
|
+
* @see packages/core/src/linux-server.ts
|
|
20
|
+
* @see packages/core/src/launch.ts (resolveHeadlessMode)
|
|
21
|
+
* @see tasks/0258 (Linux server env auto-detection)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, it } from "bun:test";
|
|
25
|
+
import { resolveHeadlessMode } from "../launch";
|
|
26
|
+
import { detectLinuxServerEnv, type LinuxServerProbes } from "../linux-server";
|
|
27
|
+
import { buildChromiumArgs, type SpawnConfig } from "../proc";
|
|
28
|
+
|
|
29
|
+
const FAKE_BINARY = "/usr/bin/chromium-stub";
|
|
30
|
+
const FAKE_UDD = "/tmp/mochi-test-udd";
|
|
31
|
+
|
|
32
|
+
function probes(overrides: Partial<LinuxServerProbes> = {}): LinuxServerProbes {
|
|
33
|
+
return {
|
|
34
|
+
platform: "linux",
|
|
35
|
+
display: undefined,
|
|
36
|
+
waylandDisplay: undefined,
|
|
37
|
+
uid: 1000,
|
|
38
|
+
hasDockerEnvFile: false,
|
|
39
|
+
cgroup: undefined,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("detectLinuxServerEnv — server-no-display classifier (task 0258)", () => {
|
|
45
|
+
it("flags Linux + no DISPLAY + no WAYLAND_DISPLAY as serverNoDisplay=true", () => {
|
|
46
|
+
const env = detectLinuxServerEnv(probes());
|
|
47
|
+
expect(env.serverNoDisplay).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("clears serverNoDisplay when DISPLAY is set (X11 dev workstation)", () => {
|
|
51
|
+
const env = detectLinuxServerEnv(probes({ display: ":0" }));
|
|
52
|
+
expect(env.serverNoDisplay).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("clears serverNoDisplay when WAYLAND_DISPLAY is set (Wayland session)", () => {
|
|
56
|
+
const env = detectLinuxServerEnv(probes({ waylandDisplay: "wayland-0" }));
|
|
57
|
+
expect(env.serverNoDisplay).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("treats empty-string DISPLAY as 'no display' (matches Chromium)", () => {
|
|
61
|
+
// An empty DISPLAY value means no usable X server; Chromium itself rejects
|
|
62
|
+
// the connection. Mirror that — an empty string must not gate us out of
|
|
63
|
+
// headless defaulting.
|
|
64
|
+
const env = detectLinuxServerEnv(probes({ display: "" }));
|
|
65
|
+
expect(env.serverNoDisplay).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("never flags serverNoDisplay on darwin / win32 (rule is Linux-only)", () => {
|
|
69
|
+
expect(detectLinuxServerEnv(probes({ platform: "darwin" })).serverNoDisplay).toBe(false);
|
|
70
|
+
expect(detectLinuxServerEnv(probes({ platform: "win32" })).serverNoDisplay).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("flags root=true when uid === 0 on Linux (orthogonal axis)", () => {
|
|
74
|
+
const env = detectLinuxServerEnv(probes({ uid: 0 }));
|
|
75
|
+
expect(env.root).toBe(true);
|
|
76
|
+
expect(env.serverNoDisplay).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("clears root when uid !== 0", () => {
|
|
80
|
+
expect(detectLinuxServerEnv(probes({ uid: 1000 })).root).toBe(false);
|
|
81
|
+
expect(detectLinuxServerEnv(probes({ uid: undefined })).root).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("never flags root on non-Linux (uid 0 on macOS root shell is not the same axis)", () => {
|
|
85
|
+
// The auto-`--no-sandbox` fallback in proc.ts is Linux-specific; the
|
|
86
|
+
// classifier mirrors that.
|
|
87
|
+
expect(detectLinuxServerEnv(probes({ platform: "darwin", uid: 0 })).root).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("flags container=true when /.dockerenv is present", () => {
|
|
91
|
+
const env = detectLinuxServerEnv(probes({ hasDockerEnvFile: true }));
|
|
92
|
+
expect(env.container).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("flags container=true when /proc/1/cgroup mentions docker", () => {
|
|
96
|
+
const env = detectLinuxServerEnv(
|
|
97
|
+
probes({ cgroup: "12:devices:/docker/abc123\n11:freezer:/docker/abc123\n" }),
|
|
98
|
+
);
|
|
99
|
+
expect(env.container).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("flags container=true when cgroup mentions containerd", () => {
|
|
103
|
+
const env = detectLinuxServerEnv(probes({ cgroup: "0::/system.slice/containerd.service\n" }));
|
|
104
|
+
expect(env.container).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("flags container=true when cgroup mentions kubepods (Kubernetes)", () => {
|
|
108
|
+
const env = detectLinuxServerEnv(
|
|
109
|
+
probes({ cgroup: "0::/kubepods.slice/kubepods-pod123.slice/\n" }),
|
|
110
|
+
);
|
|
111
|
+
expect(env.container).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("clears container when neither indicator hits", () => {
|
|
115
|
+
expect(
|
|
116
|
+
detectLinuxServerEnv(probes({ cgroup: "0::/user.slice/user-1000.slice\n" })).container,
|
|
117
|
+
).toBe(false);
|
|
118
|
+
expect(detectLinuxServerEnv(probes()).container).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("rationale string surfaces every probed axis for debug logging", () => {
|
|
122
|
+
const env = detectLinuxServerEnv(
|
|
123
|
+
probes({ display: ":0", uid: 0, hasDockerEnvFile: true, waylandDisplay: undefined }),
|
|
124
|
+
);
|
|
125
|
+
expect(env.rationale).toContain("platform=linux");
|
|
126
|
+
expect(env.rationale).toContain("display=:0");
|
|
127
|
+
expect(env.rationale).toContain("uid=0");
|
|
128
|
+
expect(env.rationale).toContain("container=true");
|
|
129
|
+
expect(env.rationale).toContain("serverNoDisplay=false");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("resolveHeadlessMode — precedence table (task 0258)", () => {
|
|
134
|
+
const SERVER_ENV = detectLinuxServerEnv(probes());
|
|
135
|
+
const DEV_ENV = detectLinuxServerEnv(probes({ display: ":0" }));
|
|
136
|
+
|
|
137
|
+
it("explicit headlessMode='new' wins on a dev workstation", () => {
|
|
138
|
+
expect(resolveHeadlessMode({ headlessMode: "new" }, DEV_ENV)).toBe("new");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("explicit headlessMode='legacy' wins on a server", () => {
|
|
142
|
+
expect(resolveHeadlessMode({ headlessMode: "legacy" }, SERVER_ENV)).toBe("legacy");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("explicit headlessMode='off' wins on a server (caller knows what they want)", () => {
|
|
146
|
+
expect(resolveHeadlessMode({ headlessMode: "off" }, SERVER_ENV)).toBe("off");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("legacy headless: true maps to 'new' regardless of env", () => {
|
|
150
|
+
expect(resolveHeadlessMode({ headless: true }, DEV_ENV)).toBe("new");
|
|
151
|
+
expect(resolveHeadlessMode({ headless: true }, SERVER_ENV)).toBe("new");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("legacy headless: false maps to 'off' regardless of env", () => {
|
|
155
|
+
// Even on a server, an explicit `headless: false` must be honored —
|
|
156
|
+
// we are not in the business of ignoring user input. The user will
|
|
157
|
+
// crash, but they asked for it.
|
|
158
|
+
expect(resolveHeadlessMode({ headless: false }, SERVER_ENV)).toBe("off");
|
|
159
|
+
expect(resolveHeadlessMode({ headless: false }, DEV_ENV)).toBe("off");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("env default: server-no-display → 'new' (the task 0258 fix)", () => {
|
|
163
|
+
expect(resolveHeadlessMode({}, SERVER_ENV)).toBe("new");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("env default: dev workstation with DISPLAY → 'off' (run headful)", () => {
|
|
167
|
+
expect(resolveHeadlessMode({}, DEV_ENV)).toBe("off");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("env default: macOS / Windows → 'off' (no Linux-only fallback)", () => {
|
|
171
|
+
const macEnv = detectLinuxServerEnv(probes({ platform: "darwin" }));
|
|
172
|
+
const winEnv = detectLinuxServerEnv(probes({ platform: "win32" }));
|
|
173
|
+
expect(resolveHeadlessMode({}, macEnv)).toBe("off");
|
|
174
|
+
expect(resolveHeadlessMode({}, winEnv)).toBe("off");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("headlessMode supersedes a contradicting legacy headless flag", () => {
|
|
178
|
+
expect(resolveHeadlessMode({ headlessMode: "off", headless: true }, SERVER_ENV)).toBe("off");
|
|
179
|
+
expect(resolveHeadlessMode({ headlessMode: "new", headless: false }, DEV_ENV)).toBe("new");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("server + root still resolves to 'new' (orthogonal axes — task 0258 §detection)", () => {
|
|
183
|
+
// The root/no-sandbox auto-flag is owned by proc.ts, not the headless
|
|
184
|
+
// resolver. This test pins that resolveHeadlessMode does NOT bake root
|
|
185
|
+
// into its decision — the user could be running the new-headless flow
|
|
186
|
+
// as root inside a container without that influencing the mode.
|
|
187
|
+
const rootServer = detectLinuxServerEnv(probes({ uid: 0 }));
|
|
188
|
+
expect(resolveHeadlessMode({}, rootServer)).toBe("new");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("container + DISPLAY (rare but valid) → 'off' (developer in a devcontainer)", () => {
|
|
192
|
+
const devcontainer = detectLinuxServerEnv(
|
|
193
|
+
probes({ display: ":0", hasDockerEnvFile: true, cgroup: "0::/docker/abc\n" }),
|
|
194
|
+
);
|
|
195
|
+
expect(devcontainer.container).toBe(true);
|
|
196
|
+
expect(devcontainer.serverNoDisplay).toBe(false);
|
|
197
|
+
expect(resolveHeadlessMode({}, devcontainer)).toBe("off");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("buildChromiumArgs — headlessMode dispatch (task 0258)", () => {
|
|
202
|
+
function baseCfg(overrides: Partial<SpawnConfig> = {}): SpawnConfig {
|
|
203
|
+
return { binary: FAKE_BINARY, headless: false, ...overrides };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
it("headlessMode='new' emits --headless=new", () => {
|
|
207
|
+
const args = buildChromiumArgs(baseCfg({ headlessMode: "new" }), FAKE_UDD, undefined);
|
|
208
|
+
expect(args).toContain("--headless=new");
|
|
209
|
+
expect(args).not.toContain("--headless");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("headlessMode='legacy' emits bare --headless", () => {
|
|
213
|
+
const args = buildChromiumArgs(baseCfg({ headlessMode: "legacy" }), FAKE_UDD, undefined);
|
|
214
|
+
expect(args).toContain("--headless");
|
|
215
|
+
expect(args).not.toContain("--headless=new");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("headlessMode='off' emits no headless flag at all", () => {
|
|
219
|
+
const args = buildChromiumArgs(baseCfg({ headlessMode: "off" }), FAKE_UDD, undefined);
|
|
220
|
+
expect(args.some((a) => a === "--headless" || a.startsWith("--headless="))).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("headlessMode='new' supersedes headless: false", () => {
|
|
224
|
+
// The launcher resolves the mode; by the time we reach buildChromiumArgs,
|
|
225
|
+
// the resolved mode is canonical.
|
|
226
|
+
const args = buildChromiumArgs(
|
|
227
|
+
baseCfg({ headless: false, headlessMode: "new" }),
|
|
228
|
+
FAKE_UDD,
|
|
229
|
+
undefined,
|
|
230
|
+
);
|
|
231
|
+
expect(args).toContain("--headless=new");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("legacy: headless: true with no headlessMode falls back to --headless=new", () => {
|
|
235
|
+
const args = buildChromiumArgs(baseCfg({ headless: true }), FAKE_UDD, undefined);
|
|
236
|
+
expect(args).toContain("--headless=new");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("legacy: headless: false with no headlessMode emits no headless flag", () => {
|
|
240
|
+
const args = buildChromiumArgs(baseCfg({ headless: false }), FAKE_UDD, undefined);
|
|
241
|
+
expect(args.some((a) => a === "--headless" || a.startsWith("--headless="))).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* with-auth/no-auth × edge cases (missing port, IPv6 host, percent-encoded
|
|
6
6
|
* creds, empty password).
|
|
7
7
|
*
|
|
8
|
-
* `installProxyAuth`: drives a fake CDP router
|
|
8
|
+
* `installProxyAuth`: drives a fake CDP router via the shared
|
|
9
|
+
* `tests/helpers/cdp-fixture.ts` helper. Verifies:
|
|
9
10
|
* - `Fetch.enable` is sent with `handleAuthRequests: true, patterns: [{ urlPattern: "*" }]`.
|
|
10
11
|
* - `Fetch.authRequired` events trigger `Fetch.continueWithAuth` carrying
|
|
11
12
|
* the configured creds.
|
|
@@ -16,8 +17,8 @@
|
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { describe, expect, it } from "bun:test";
|
|
20
|
+
import { type FakePipe, makeFakePipe } from "../../../../tests/helpers/cdp-fixture";
|
|
19
21
|
import { MessageRouter } from "../cdp/router";
|
|
20
|
-
import type { PipeReader, PipeWriter } from "../cdp/transport";
|
|
21
22
|
import { installProxyAuth, parseProxyUrl } from "../proxy-auth";
|
|
22
23
|
|
|
23
24
|
describe("parseProxyUrl", () => {
|
|
@@ -131,69 +132,35 @@ describe("parseProxyUrl", () => {
|
|
|
131
132
|
|
|
132
133
|
interface FakeRouter {
|
|
133
134
|
router: MessageRouter;
|
|
134
|
-
|
|
135
|
+
pipe: FakePipe;
|
|
135
136
|
pushEvent(method: string, params: unknown): void;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
function makeRouter(): FakeRouter {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
pumpController = c;
|
|
144
|
-
},
|
|
145
|
-
});
|
|
146
|
-
const reader: PipeReader = {
|
|
147
|
-
getReader: () => stream.getReader(),
|
|
148
|
-
};
|
|
149
|
-
const writer: PipeWriter = {
|
|
150
|
-
write: (chunk: Uint8Array) => {
|
|
151
|
-
const last = chunk[chunk.length - 1] === 0 ? chunk.length - 1 : chunk.length;
|
|
152
|
-
const json = new TextDecoder().decode(chunk.subarray(0, last));
|
|
153
|
-
try {
|
|
154
|
-
const obj = JSON.parse(json) as { id?: number; method: string; params?: unknown };
|
|
155
|
-
written.push({ method: obj.method, params: obj.params });
|
|
156
|
-
// Auto-resolve the request immediately so the await in
|
|
157
|
-
// `installProxyAuth` doesn't hang.
|
|
158
|
-
if (typeof obj.id === "number") {
|
|
159
|
-
const reply = JSON.stringify({ id: obj.id, result: {} });
|
|
160
|
-
const enc = new TextEncoder().encode(reply);
|
|
161
|
-
const out = new Uint8Array(enc.length + 1);
|
|
162
|
-
out.set(enc, 0);
|
|
163
|
-
out[enc.length] = 0;
|
|
164
|
-
pumpController?.enqueue(out);
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
// ignore
|
|
168
|
-
}
|
|
169
|
-
return chunk.byteLength;
|
|
170
|
-
},
|
|
171
|
-
flush: () => undefined,
|
|
172
|
-
end: () => undefined,
|
|
173
|
-
};
|
|
174
|
-
const router = new MessageRouter(reader, writer);
|
|
140
|
+
// Default responders auto-answer Fetch.enable / Fetch.disable / etc with
|
|
141
|
+
// `{}` — that's everything `installProxyAuth` waits on.
|
|
142
|
+
const pipe = makeFakePipe();
|
|
143
|
+
const router = new MessageRouter(pipe.reader, pipe.writer);
|
|
175
144
|
router.start();
|
|
176
|
-
const enc = new TextEncoder();
|
|
177
145
|
return {
|
|
178
146
|
router,
|
|
179
|
-
|
|
147
|
+
pipe,
|
|
180
148
|
pushEvent(method: string, params: unknown): void {
|
|
181
|
-
|
|
182
|
-
const out = new Uint8Array(bytes.length + 1);
|
|
183
|
-
out.set(bytes, 0);
|
|
184
|
-
out[bytes.length] = 0;
|
|
185
|
-
pumpController?.enqueue(out);
|
|
149
|
+
pipe.inject({ method, params });
|
|
186
150
|
},
|
|
187
151
|
};
|
|
188
152
|
}
|
|
189
153
|
|
|
190
154
|
describe("installProxyAuth", () => {
|
|
191
|
-
it("sends Fetch.enable with handleAuthRequests:true and
|
|
155
|
+
it("sends Fetch.enable with handleAuthRequests:true and Document-first patterns (task 0266)", async () => {
|
|
192
156
|
const f = makeRouter();
|
|
193
157
|
const handle = await installProxyAuth(f.router, { username: "u", password: "p" });
|
|
194
|
-
const enable = f.written.find((c) => c.method === "Fetch.enable");
|
|
158
|
+
const enable = f.pipe.written.find((c) => c.parsed.method === "Fetch.enable");
|
|
195
159
|
expect(enable).toBeDefined();
|
|
196
|
-
expect(enable?.params).toEqual({
|
|
160
|
+
expect(enable?.parsed.params).toEqual({
|
|
161
|
+
handleAuthRequests: true,
|
|
162
|
+
patterns: [{ urlPattern: "*", resourceType: "Document" }, { urlPattern: "*" }],
|
|
163
|
+
});
|
|
197
164
|
await handle.dispose();
|
|
198
165
|
await f.router.close();
|
|
199
166
|
});
|
|
@@ -204,9 +171,9 @@ describe("installProxyAuth", () => {
|
|
|
204
171
|
f.pushEvent("Fetch.authRequired", { requestId: "req-42", authChallenge: { source: "Proxy" } });
|
|
205
172
|
// Allow microtasks + the writer push to flush.
|
|
206
173
|
await new Promise((r) => setTimeout(r, 10));
|
|
207
|
-
const reply = f.written.find((c) => c.method === "Fetch.continueWithAuth");
|
|
174
|
+
const reply = f.pipe.written.find((c) => c.parsed.method === "Fetch.continueWithAuth");
|
|
208
175
|
expect(reply).toBeDefined();
|
|
209
|
-
expect(reply?.params).toEqual({
|
|
176
|
+
expect(reply?.parsed.params).toEqual({
|
|
210
177
|
requestId: "req-42",
|
|
211
178
|
authChallengeResponse: {
|
|
212
179
|
response: "ProvideCredentials",
|
|
@@ -223,9 +190,9 @@ describe("installProxyAuth", () => {
|
|
|
223
190
|
const handle = await installProxyAuth(f.router, { username: "u", password: "p" });
|
|
224
191
|
f.pushEvent("Fetch.requestPaused", { requestId: "rp-1" });
|
|
225
192
|
await new Promise((r) => setTimeout(r, 10));
|
|
226
|
-
const reply = f.written.find((c) => c.method === "Fetch.continueRequest");
|
|
193
|
+
const reply = f.pipe.written.find((c) => c.parsed.method === "Fetch.continueRequest");
|
|
227
194
|
expect(reply).toBeDefined();
|
|
228
|
-
expect(reply?.params).toEqual({ requestId: "rp-1" });
|
|
195
|
+
expect(reply?.parsed.params).toEqual({ requestId: "rp-1" });
|
|
229
196
|
await handle.dispose();
|
|
230
197
|
await f.router.close();
|
|
231
198
|
});
|
|
@@ -235,7 +202,7 @@ describe("installProxyAuth", () => {
|
|
|
235
202
|
const handle = await installProxyAuth(f.router, { username: "u", password: "p" });
|
|
236
203
|
await handle.dispose();
|
|
237
204
|
await handle.dispose();
|
|
238
|
-
const disables = f.written.filter((c) => c.method === "Fetch.disable");
|
|
205
|
+
const disables = f.pipe.written.filter((c) => c.parsed.method === "Fetch.disable");
|
|
239
206
|
expect(disables.length).toBe(1);
|
|
240
207
|
await f.router.close();
|
|
241
208
|
});
|
|
@@ -246,7 +213,7 @@ describe("installProxyAuth", () => {
|
|
|
246
213
|
await handle.dispose();
|
|
247
214
|
f.pushEvent("Fetch.authRequired", { requestId: "late" });
|
|
248
215
|
await new Promise((r) => setTimeout(r, 10));
|
|
249
|
-
const replies = f.written.filter((c) => c.method === "Fetch.continueWithAuth");
|
|
216
|
+
const replies = f.pipe.written.filter((c) => c.parsed.method === "Fetch.continueWithAuth");
|
|
250
217
|
expect(replies.length).toBe(0);
|
|
251
218
|
await f.router.close();
|
|
252
219
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live conformance test for `Page.screenshot` (task 0265).
|
|
3
|
+
*
|
|
4
|
+
* Gated by `MOCHI_E2E=1` so the default `bun test` run stays fast and offline.
|
|
5
|
+
* Spawns a real Chromium-for-Testing instance, navigates to a tiny data URL,
|
|
6
|
+
* captures the viewport as PNG, and asserts the bytes start with the PNG
|
|
7
|
+
* magic signature.
|
|
8
|
+
*
|
|
9
|
+
* Budget: < 10 seconds.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { mochi } from "../index";
|
|
14
|
+
|
|
15
|
+
const E2E_ENABLED = process.env.MOCHI_E2E === "1";
|
|
16
|
+
const TEST_TIMEOUT_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
const describeOrSkip = E2E_ENABLED ? describe : describe.skip;
|
|
19
|
+
|
|
20
|
+
describeOrSkip("@mochi.js/core Page.screenshot E2E (MOCHI_E2E=1)", () => {
|
|
21
|
+
it(
|
|
22
|
+
"captures a PNG screenshot — Uint8Array starts with PNG magic bytes",
|
|
23
|
+
async () => {
|
|
24
|
+
const session = await mochi.launch({
|
|
25
|
+
profile: "test",
|
|
26
|
+
seed: "screenshot-e2e",
|
|
27
|
+
headless: true,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const page = await session.newPage();
|
|
31
|
+
await page.goto("data:text/html,<title>shot</title><h1 style='color:red'>hello</h1>");
|
|
32
|
+
const png = await page.screenshot();
|
|
33
|
+
expect(png).toBeInstanceOf(Uint8Array);
|
|
34
|
+
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
|
35
|
+
expect(png[0]).toBe(0x89);
|
|
36
|
+
expect(png[1]).toBe(0x50);
|
|
37
|
+
expect(png[2]).toBe(0x4e);
|
|
38
|
+
expect(png[3]).toBe(0x47);
|
|
39
|
+
expect(png[4]).toBe(0x0d);
|
|
40
|
+
expect(png[5]).toBe(0x0a);
|
|
41
|
+
expect(png[6]).toBe(0x1a);
|
|
42
|
+
expect(png[7]).toBe(0x0a);
|
|
43
|
+
// Sanity: a non-trivial image should be more than just the header.
|
|
44
|
+
expect(png.length).toBeGreaterThan(100);
|
|
45
|
+
} finally {
|
|
46
|
+
await session.close();
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
TEST_TIMEOUT_MS,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
it(
|
|
53
|
+
"captures a JPEG screenshot when format: 'jpeg'",
|
|
54
|
+
async () => {
|
|
55
|
+
const session = await mochi.launch({
|
|
56
|
+
profile: "test",
|
|
57
|
+
seed: "screenshot-e2e-jpeg",
|
|
58
|
+
headless: true,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const page = await session.newPage();
|
|
62
|
+
await page.goto("data:text/html,<h1>jpeg</h1>");
|
|
63
|
+
const jpeg = await page.screenshot({ format: "jpeg", quality: 70 });
|
|
64
|
+
expect(jpeg).toBeInstanceOf(Uint8Array);
|
|
65
|
+
// JPEG SOI marker: FF D8 FF
|
|
66
|
+
expect(jpeg[0]).toBe(0xff);
|
|
67
|
+
expect(jpeg[1]).toBe(0xd8);
|
|
68
|
+
expect(jpeg[2]).toBe(0xff);
|
|
69
|
+
} finally {
|
|
70
|
+
await session.close();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
TEST_TIMEOUT_MS,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
it(
|
|
77
|
+
"fullPage: true captures beyond the visible viewport",
|
|
78
|
+
async () => {
|
|
79
|
+
const session = await mochi.launch({
|
|
80
|
+
profile: "test",
|
|
81
|
+
seed: "screenshot-e2e-fullpage",
|
|
82
|
+
headless: true,
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const page = await session.newPage();
|
|
86
|
+
// A page taller than the default viewport so fullPage actually matters.
|
|
87
|
+
await page.goto(
|
|
88
|
+
"data:text/html,<style>body{margin:0}div{height:3000px;background:linear-gradient(red,blue)}</style><div></div>",
|
|
89
|
+
);
|
|
90
|
+
const viewportShot = await page.screenshot();
|
|
91
|
+
const fullShot = await page.screenshot({ fullPage: true });
|
|
92
|
+
expect(viewportShot[0]).toBe(0x89);
|
|
93
|
+
expect(fullShot[0]).toBe(0x89);
|
|
94
|
+
// Full-page bytes should be larger than viewport bytes for a 3000px page.
|
|
95
|
+
expect(fullShot.length).toBeGreaterThan(viewportShot.length);
|
|
96
|
+
} finally {
|
|
97
|
+
await session.close();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
TEST_TIMEOUT_MS,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it(
|
|
104
|
+
"encoding: 'base64' returns a string instead of bytes",
|
|
105
|
+
async () => {
|
|
106
|
+
const session = await mochi.launch({
|
|
107
|
+
profile: "test",
|
|
108
|
+
seed: "screenshot-e2e-b64",
|
|
109
|
+
headless: true,
|
|
110
|
+
});
|
|
111
|
+
try {
|
|
112
|
+
const page = await session.newPage();
|
|
113
|
+
await page.goto("data:text/html,<h1>b64</h1>");
|
|
114
|
+
const b64 = await page.screenshot({ encoding: "base64" });
|
|
115
|
+
expect(typeof b64).toBe("string");
|
|
116
|
+
// Decoded base64 must start with the PNG magic.
|
|
117
|
+
const decoded = Buffer.from(b64, "base64");
|
|
118
|
+
expect(decoded[0]).toBe(0x89);
|
|
119
|
+
expect(decoded[1]).toBe(0x50);
|
|
120
|
+
} finally {
|
|
121
|
+
await session.close();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
TEST_TIMEOUT_MS,
|
|
125
|
+
);
|
|
126
|
+
});
|