@mochi.js/core 0.2.2 → 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__/geo-consistency.test.ts +277 -0
- package/src/__tests__/geo-probe.test.ts +415 -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 -162
- package/src/__tests__/integration.e2e.test.ts +24 -0
- 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/geo-consistency.ts +343 -0
- package/src/geo-probe.ts +603 -0
- package/src/index.ts +43 -1
- package/src/launch.ts +277 -17
- package/src/linux-server.ts +157 -0
- package/src/page.ts +420 -9
- package/src/proc.ts +48 -5
- package/src/proxy-auth.ts +26 -107
- package/src/session.ts +595 -78
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `Page.screenshot` (task 0265).
|
|
3
|
+
*
|
|
4
|
+
* Exercises the CDP wire shape against a `MessageRouter` driven over a fake
|
|
5
|
+
* pipe — no real Chromium spawn, no real `Session`. The router still applies
|
|
6
|
+
* the §8.2 forbidden-method assertion on every send, so this test also
|
|
7
|
+
* implicitly verifies that `Page.captureScreenshot` is NOT on the forbidden
|
|
8
|
+
* list.
|
|
9
|
+
*
|
|
10
|
+
* Coverage:
|
|
11
|
+
* - PNG/JPEG params (format, quality)
|
|
12
|
+
* - clip with default scale
|
|
13
|
+
* - omitBackground passthrough
|
|
14
|
+
* - encoding: "base64" returns the raw string
|
|
15
|
+
* - encoding: "binary" (default) decodes to a Uint8Array — verified by
|
|
16
|
+
* constructing a known byte sequence, base64-encoding it, and asserting
|
|
17
|
+
* round-trip equality including the PNG magic bytes
|
|
18
|
+
* - fullPage: drives `Page.getLayoutMetrics` + `setDeviceMetricsOverride`
|
|
19
|
+
* + capture + `clearDeviceMetricsOverride` (in that order)
|
|
20
|
+
* - fullPage cleanup: `clearDeviceMetricsOverride` is called even when the
|
|
21
|
+
* capture itself rejects
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, it } from "bun:test";
|
|
25
|
+
import { MessageRouter } from "../cdp/router";
|
|
26
|
+
import type { PipeReader, PipeWriter } from "../cdp/transport";
|
|
27
|
+
import { Page } from "../page";
|
|
28
|
+
|
|
29
|
+
interface RecordedRequest {
|
|
30
|
+
id: number;
|
|
31
|
+
method: string;
|
|
32
|
+
params?: unknown;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FakeRouter {
|
|
37
|
+
router: MessageRouter;
|
|
38
|
+
requests: RecordedRequest[];
|
|
39
|
+
/** Auto-respond to a method with a result (or error). */
|
|
40
|
+
on(method: string, responder: (req: RecordedRequest) => unknown | { __error: string }): void;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeFakeRouter(): FakeRouter {
|
|
45
|
+
const requests: RecordedRequest[] = [];
|
|
46
|
+
let pumpController: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
47
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
48
|
+
start(c) {
|
|
49
|
+
pumpController = c;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const enc = new TextEncoder();
|
|
53
|
+
const dec = new TextDecoder();
|
|
54
|
+
const responders: Array<{
|
|
55
|
+
method: string;
|
|
56
|
+
fn: (req: RecordedRequest) => unknown | { __error: string };
|
|
57
|
+
}> = [];
|
|
58
|
+
|
|
59
|
+
const push = (obj: unknown): void => {
|
|
60
|
+
const bytes = enc.encode(JSON.stringify(obj));
|
|
61
|
+
const out = new Uint8Array(bytes.length + 1);
|
|
62
|
+
out.set(bytes, 0);
|
|
63
|
+
out[bytes.length] = 0;
|
|
64
|
+
pumpController?.enqueue(out);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const reader: PipeReader = {
|
|
68
|
+
getReader: () => stream.getReader(),
|
|
69
|
+
};
|
|
70
|
+
const writer: PipeWriter = {
|
|
71
|
+
write: (chunk) => {
|
|
72
|
+
const last = chunk[chunk.length - 1] === 0 ? chunk.length - 1 : chunk.length;
|
|
73
|
+
const json = dec.decode(chunk.subarray(0, last));
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(json) as RecordedRequest;
|
|
76
|
+
requests.push(parsed);
|
|
77
|
+
const r = responders.find((x) => x.method === parsed.method);
|
|
78
|
+
if (r !== undefined && typeof parsed.id === "number") {
|
|
79
|
+
queueMicrotask(() => {
|
|
80
|
+
const result = r.fn(parsed);
|
|
81
|
+
if (
|
|
82
|
+
result !== null &&
|
|
83
|
+
typeof result === "object" &&
|
|
84
|
+
"__error" in result &&
|
|
85
|
+
typeof (result as { __error: string }).__error === "string"
|
|
86
|
+
) {
|
|
87
|
+
push({
|
|
88
|
+
id: parsed.id,
|
|
89
|
+
error: { code: -32000, message: (result as { __error: string }).__error },
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
push({ id: parsed.id, result });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
flush: () => undefined,
|
|
101
|
+
end: () => undefined,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const router = new MessageRouter(reader, writer);
|
|
105
|
+
router.start();
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
router,
|
|
109
|
+
requests,
|
|
110
|
+
on(method, fn) {
|
|
111
|
+
responders.push({ method, fn });
|
|
112
|
+
},
|
|
113
|
+
async close() {
|
|
114
|
+
pumpController?.close();
|
|
115
|
+
await router.close();
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Build a Page wired to the fake router, no inject script. */
|
|
121
|
+
function makePage(fake: FakeRouter): Page {
|
|
122
|
+
return new Page({
|
|
123
|
+
router: fake.router,
|
|
124
|
+
targetId: "target-1",
|
|
125
|
+
sessionId: "session-1",
|
|
126
|
+
initialUrl: "about:blank",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe("Page.screenshot — CDP wire shape", () => {
|
|
131
|
+
it("default opts → Page.captureScreenshot { format: 'png' }, decodes base64 to Uint8Array", async () => {
|
|
132
|
+
const fake = makeFakeRouter();
|
|
133
|
+
try {
|
|
134
|
+
// Construct a known byte sequence (the PNG magic + a few payload bytes)
|
|
135
|
+
// and base64-encode it. The screenshot decoder should round-trip this
|
|
136
|
+
// exactly back to a Uint8Array.
|
|
137
|
+
const knownBytes = new Uint8Array([
|
|
138
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xde, 0xad,
|
|
139
|
+
]);
|
|
140
|
+
const b64 = Buffer.from(knownBytes).toString("base64");
|
|
141
|
+
fake.on("Page.captureScreenshot", () => ({ data: b64 }));
|
|
142
|
+
|
|
143
|
+
const page = makePage(fake);
|
|
144
|
+
const result = await page.screenshot();
|
|
145
|
+
|
|
146
|
+
expect(result).toBeInstanceOf(Uint8Array);
|
|
147
|
+
expect(Array.from(result)).toEqual(Array.from(knownBytes));
|
|
148
|
+
// PNG magic bytes
|
|
149
|
+
expect(result[0]).toBe(0x89);
|
|
150
|
+
expect(result[1]).toBe(0x50);
|
|
151
|
+
expect(result[2]).toBe(0x4e);
|
|
152
|
+
expect(result[3]).toBe(0x47);
|
|
153
|
+
|
|
154
|
+
const captureReq = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
155
|
+
expect(captureReq).toBeDefined();
|
|
156
|
+
expect(captureReq?.params).toEqual({ format: "png" });
|
|
157
|
+
expect(captureReq?.sessionId).toBe("session-1");
|
|
158
|
+
} finally {
|
|
159
|
+
await fake.close();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("encoding: 'base64' returns the raw CDP string (no decode)", async () => {
|
|
164
|
+
const fake = makeFakeRouter();
|
|
165
|
+
try {
|
|
166
|
+
const knownBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
|
|
167
|
+
const b64 = Buffer.from(knownBytes).toString("base64");
|
|
168
|
+
fake.on("Page.captureScreenshot", () => ({ data: b64 }));
|
|
169
|
+
|
|
170
|
+
const page = makePage(fake);
|
|
171
|
+
const result = await page.screenshot({ encoding: "base64" });
|
|
172
|
+
expect(typeof result).toBe("string");
|
|
173
|
+
expect(result).toBe(b64);
|
|
174
|
+
} finally {
|
|
175
|
+
await fake.close();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("format: 'jpeg' + quality → params carry both", async () => {
|
|
180
|
+
const fake = makeFakeRouter();
|
|
181
|
+
try {
|
|
182
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
183
|
+
const page = makePage(fake);
|
|
184
|
+
await page.screenshot({ format: "jpeg", quality: 80 });
|
|
185
|
+
|
|
186
|
+
const req = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
187
|
+
expect(req?.params).toEqual({ format: "jpeg", quality: 80 });
|
|
188
|
+
} finally {
|
|
189
|
+
await fake.close();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("format: 'png' + quality → quality is dropped (PNG has no quality knob)", async () => {
|
|
194
|
+
const fake = makeFakeRouter();
|
|
195
|
+
try {
|
|
196
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
197
|
+
const page = makePage(fake);
|
|
198
|
+
await page.screenshot({ format: "png", quality: 80 });
|
|
199
|
+
|
|
200
|
+
const req = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
201
|
+
expect(req?.params).toEqual({ format: "png" });
|
|
202
|
+
} finally {
|
|
203
|
+
await fake.close();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("clip without scale → defaults scale to 1", async () => {
|
|
208
|
+
const fake = makeFakeRouter();
|
|
209
|
+
try {
|
|
210
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
211
|
+
const page = makePage(fake);
|
|
212
|
+
await page.screenshot({ clip: { x: 10, y: 20, width: 100, height: 50 } });
|
|
213
|
+
|
|
214
|
+
const req = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
215
|
+
expect(req?.params).toEqual({
|
|
216
|
+
format: "png",
|
|
217
|
+
clip: { x: 10, y: 20, width: 100, height: 50, scale: 1 },
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
await fake.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("clip with explicit scale is preserved", async () => {
|
|
225
|
+
const fake = makeFakeRouter();
|
|
226
|
+
try {
|
|
227
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
228
|
+
const page = makePage(fake);
|
|
229
|
+
await page.screenshot({ clip: { x: 0, y: 0, width: 50, height: 50, scale: 2 } });
|
|
230
|
+
|
|
231
|
+
const req = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
232
|
+
expect(req?.params).toEqual({
|
|
233
|
+
format: "png",
|
|
234
|
+
clip: { x: 0, y: 0, width: 50, height: 50, scale: 2 },
|
|
235
|
+
});
|
|
236
|
+
} finally {
|
|
237
|
+
await fake.close();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("omitBackground passes through to params", async () => {
|
|
242
|
+
const fake = makeFakeRouter();
|
|
243
|
+
try {
|
|
244
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
245
|
+
const page = makePage(fake);
|
|
246
|
+
await page.screenshot({ omitBackground: true });
|
|
247
|
+
|
|
248
|
+
const req = fake.requests.find((r) => r.method === "Page.captureScreenshot");
|
|
249
|
+
expect(req?.params).toEqual({ format: "png", omitBackground: true });
|
|
250
|
+
} finally {
|
|
251
|
+
await fake.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("fullPage: true → getLayoutMetrics + setDeviceMetricsOverride + capture + clearDeviceMetricsOverride", async () => {
|
|
256
|
+
const fake = makeFakeRouter();
|
|
257
|
+
try {
|
|
258
|
+
fake.on("Page.getLayoutMetrics", () => ({
|
|
259
|
+
contentSize: { width: 1280, height: 4321 },
|
|
260
|
+
layoutViewport: { clientWidth: 1280, clientHeight: 800 },
|
|
261
|
+
}));
|
|
262
|
+
fake.on("Emulation.setDeviceMetricsOverride", () => ({}));
|
|
263
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
264
|
+
fake.on("Emulation.clearDeviceMetricsOverride", () => ({}));
|
|
265
|
+
|
|
266
|
+
const page = makePage(fake);
|
|
267
|
+
await page.screenshot({ fullPage: true });
|
|
268
|
+
|
|
269
|
+
const methods = fake.requests.map((r) => r.method);
|
|
270
|
+
// The four CDP methods must appear in this exact relative order.
|
|
271
|
+
const idxMetrics = methods.indexOf("Page.getLayoutMetrics");
|
|
272
|
+
const idxOverride = methods.indexOf("Emulation.setDeviceMetricsOverride");
|
|
273
|
+
const idxCapture = methods.indexOf("Page.captureScreenshot");
|
|
274
|
+
const idxClear = methods.indexOf("Emulation.clearDeviceMetricsOverride");
|
|
275
|
+
expect(idxMetrics).toBeGreaterThanOrEqual(0);
|
|
276
|
+
expect(idxOverride).toBeGreaterThan(idxMetrics);
|
|
277
|
+
expect(idxCapture).toBeGreaterThan(idxOverride);
|
|
278
|
+
expect(idxClear).toBeGreaterThan(idxCapture);
|
|
279
|
+
|
|
280
|
+
const overrideReq = fake.requests[idxOverride];
|
|
281
|
+
expect(overrideReq?.params).toEqual({
|
|
282
|
+
width: 1280,
|
|
283
|
+
height: 4321,
|
|
284
|
+
deviceScaleFactor: 0,
|
|
285
|
+
mobile: false,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// captureBeyondViewport must be set on the capture call so the renderer
|
|
289
|
+
// paints past the visible area for the duration of the capture.
|
|
290
|
+
const captureReq = fake.requests[idxCapture];
|
|
291
|
+
expect(captureReq?.params).toEqual({
|
|
292
|
+
format: "png",
|
|
293
|
+
captureBeyondViewport: true,
|
|
294
|
+
});
|
|
295
|
+
} finally {
|
|
296
|
+
await fake.close();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("fullPage cleanup: clearDeviceMetricsOverride runs even when capture rejects", async () => {
|
|
301
|
+
const fake = makeFakeRouter();
|
|
302
|
+
try {
|
|
303
|
+
fake.on("Page.getLayoutMetrics", () => ({
|
|
304
|
+
contentSize: { width: 1280, height: 4321 },
|
|
305
|
+
layoutViewport: { clientWidth: 1280, clientHeight: 800 },
|
|
306
|
+
}));
|
|
307
|
+
fake.on("Emulation.setDeviceMetricsOverride", () => ({}));
|
|
308
|
+
fake.on("Page.captureScreenshot", () => ({ __error: "Target detached mid-capture" }));
|
|
309
|
+
fake.on("Emulation.clearDeviceMetricsOverride", () => ({}));
|
|
310
|
+
|
|
311
|
+
const page = makePage(fake);
|
|
312
|
+
let caught: unknown;
|
|
313
|
+
try {
|
|
314
|
+
await page.screenshot({ fullPage: true });
|
|
315
|
+
} catch (err) {
|
|
316
|
+
caught = err;
|
|
317
|
+
}
|
|
318
|
+
expect(caught).toBeDefined();
|
|
319
|
+
|
|
320
|
+
const methods = fake.requests.map((r) => r.method);
|
|
321
|
+
// Despite the capture rejection, the clear must still have been sent.
|
|
322
|
+
expect(methods).toContain("Emulation.clearDeviceMetricsOverride");
|
|
323
|
+
} finally {
|
|
324
|
+
await fake.close();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("fullPage + clip → clip wins, no device-metrics round-trip", async () => {
|
|
329
|
+
const fake = makeFakeRouter();
|
|
330
|
+
try {
|
|
331
|
+
fake.on("Page.captureScreenshot", () => ({ data: "AA==" }));
|
|
332
|
+
const page = makePage(fake);
|
|
333
|
+
await page.screenshot({ fullPage: true, clip: { x: 0, y: 0, width: 10, height: 10 } });
|
|
334
|
+
|
|
335
|
+
const methods = fake.requests.map((r) => r.method);
|
|
336
|
+
expect(methods).not.toContain("Page.getLayoutMetrics");
|
|
337
|
+
expect(methods).not.toContain("Emulation.setDeviceMetricsOverride");
|
|
338
|
+
expect(methods).toContain("Page.captureScreenshot");
|
|
339
|
+
} finally {
|
|
340
|
+
await fake.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("rejects after the page has been closed", async () => {
|
|
345
|
+
const fake = makeFakeRouter();
|
|
346
|
+
try {
|
|
347
|
+
fake.on("Target.closeTarget", () => ({ success: true }));
|
|
348
|
+
const page = makePage(fake);
|
|
349
|
+
await page.close();
|
|
350
|
+
|
|
351
|
+
let caught: unknown;
|
|
352
|
+
try {
|
|
353
|
+
await page.screenshot();
|
|
354
|
+
} catch (err) {
|
|
355
|
+
caught = err;
|
|
356
|
+
}
|
|
357
|
+
expect(caught).toBeDefined();
|
|
358
|
+
expect((caught as Error).message).toMatch(/page is closed/);
|
|
359
|
+
} finally {
|
|
360
|
+
await fake.close();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|