browser-commander 0.32.1 → 0.33.1
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 +16 -0
- package/README.md +52 -5
- package/package.json +1 -1
- package/src/browser/browser-handoff.js +4 -0
- package/src/browser/chrome-for-testing-cache.js +198 -0
- package/src/browser/launch-executable.js +203 -0
- package/src/browser/launch-restrictions.json +7 -1
- package/src/browser/launcher.js +78 -65
- package/src/browser/migration/firefox.js +1 -1
- package/src/browser/migration/index.js +10 -0
- package/src/browser/migration/os-crypt-keys.js +36 -6
- package/src/browser/migration/passwords.js +1 -1
- package/src/browser/parity.js +6 -1
- package/src/browser/real-browser.js +53 -14
- package/src/browser/webdriver.js +63 -8
- package/src/capture/screenshot.js +16 -1
- package/src/capture/viewport.js +2 -1
- package/src/exports.js +5 -0
- package/src/traces/recorder.js +18 -2
- package/tests/e2e/chrome-for-testing.e2e.test.js +105 -0
- package/tests/e2e/startup-readiness.e2e.test.js +2 -0
- package/tests/unit/browser/browser-handoff.test.js +13 -0
- package/tests/unit/browser/chrome-for-testing-cache.test.js +139 -0
- package/tests/unit/browser/launch-executable.test.js +94 -0
- package/tests/unit/browser/launcher.test.js +235 -31
- package/tests/unit/browser/migration/os-crypt-keys.test.js +57 -0
- package/tests/unit/browser/migration/safari-data.test.js +43 -19
- package/tests/unit/browser/parity.test.js +19 -0
- package/tests/unit/browser/real-browser.test.js +125 -0
- package/tests/unit/browser/webdriver.test.js +95 -14
- package/tests/unit/scripts/check-web-archive.test.js +77 -2
- package/tests/unit/traces/issue-162-defaults.test.js +1 -1
- package/tests/unit/traces/issue-166-policy.test.js +1 -1
- package/tests/unit/traces/issue-180-screenshots.test.js +173 -0
|
@@ -166,6 +166,31 @@ function launchDependencies(overrides = {}) {
|
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Resolve through a fake Selenium Manager that answers browserPath.
|
|
170
|
+
async function managerResolve(
|
|
171
|
+
options,
|
|
172
|
+
{ checkAccess = accessOnly(), browserPath }
|
|
173
|
+
) {
|
|
174
|
+
const commands = [];
|
|
175
|
+
const resolved = await resolveWebDriverExecutable({
|
|
176
|
+
managerPath: '/sm',
|
|
177
|
+
checkAccess,
|
|
178
|
+
runCommand: async (file, args) => {
|
|
179
|
+
commands.push([file, ...args]);
|
|
180
|
+
return managerOutput({
|
|
181
|
+
driver_path: '/cache/chromedriver',
|
|
182
|
+
browser_path: browserPath,
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
...options,
|
|
186
|
+
});
|
|
187
|
+
return { commands, resolved };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const SELENIUM_CFT = '/home/u/.cache/selenium/chrome/linux64/156.0/chrome';
|
|
191
|
+
const PLAYWRIGHT_CFT =
|
|
192
|
+
'/home/u/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome';
|
|
193
|
+
|
|
169
194
|
describe('resolveWebDriverExecutable', () => {
|
|
170
195
|
it('uses an explicit driverPath first', async () => {
|
|
171
196
|
const resolved = await resolveWebDriverExecutable({
|
|
@@ -210,21 +235,38 @@ describe('resolveWebDriverExecutable', () => {
|
|
|
210
235
|
assert.equal(resolved.driverPath, 'C:\\b\\chromedriver.exe');
|
|
211
236
|
});
|
|
212
237
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
// A chromedriver on PATH drives the installed Google Chrome: never used
|
|
239
|
+
// for Chrome for Testing (issue #179). Selenium Manager returns a PATH
|
|
240
|
+
// driver of another release too, so it is told to skip it (run
|
|
241
|
+
// 39651456089: ChromeDriver 155 for Chrome 153).
|
|
242
|
+
for (const cft of [PLAYWRIGHT_CFT, SELENIUM_CFT]) {
|
|
243
|
+
it(`matches ${cft} with its own driver, not the one on PATH (issue #179)`, async () => {
|
|
244
|
+
const { commands, resolved } = await managerResolve(
|
|
245
|
+
{ executablePath: cft, environment: { PATH: '/bin' } },
|
|
246
|
+
{ checkAccess: accessOnly('/bin/chromedriver'), browserPath: cft }
|
|
247
|
+
);
|
|
248
|
+
assert.deepEqual(commands, [
|
|
249
|
+
[
|
|
250
|
+
'/sm',
|
|
251
|
+
'--browser',
|
|
252
|
+
'chrome',
|
|
253
|
+
'--output',
|
|
254
|
+
'json',
|
|
255
|
+
'--browser-path',
|
|
256
|
+
cft,
|
|
257
|
+
'--skip-driver-in-path',
|
|
258
|
+
],
|
|
259
|
+
]);
|
|
260
|
+
assert.equal(resolved.driverPath, '/cache/chromedriver');
|
|
261
|
+
assert.equal(resolved.source, 'selenium-manager');
|
|
227
262
|
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
it('then asks Selenium Manager for a matching driver', async () => {
|
|
266
|
+
const { commands, resolved } = await managerResolve(
|
|
267
|
+
{ executablePath: '/usr/bin/google-chrome', environment: { PATH: '' } },
|
|
268
|
+
{ browserPath: '/usr/bin/google-chrome' }
|
|
269
|
+
);
|
|
228
270
|
assert.deepEqual(commands, [
|
|
229
271
|
[
|
|
230
272
|
'/sm',
|
|
@@ -381,6 +423,45 @@ describe('launchWebDriver', () => {
|
|
|
381
423
|
await session.close(); // idempotent
|
|
382
424
|
});
|
|
383
425
|
|
|
426
|
+
it('adds the Chrome for Testing defaults for the browser it resolves (issue #179)', async () => {
|
|
427
|
+
const launchWith = async (browserPath, options = {}) => {
|
|
428
|
+
const { mock, dependencies } = launchDependencies({
|
|
429
|
+
runCommand: async () =>
|
|
430
|
+
managerOutput({
|
|
431
|
+
driver_path: '/cache/chromedriver',
|
|
432
|
+
browser_path: browserPath,
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
const session = await launchWebDriver(options, dependencies);
|
|
436
|
+
await session.close();
|
|
437
|
+
const { args } = mock.record.builders[0].options;
|
|
438
|
+
return {
|
|
439
|
+
defaults: session.defaultRestrictions,
|
|
440
|
+
switches: args.filter((arg) =>
|
|
441
|
+
['--test-type=gpu', '--use-mock-keychain'].includes(arg)
|
|
442
|
+
),
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Selenium Manager downloads Chrome for Testing when no Chrome is installed.
|
|
447
|
+
const cft = await launchWith(
|
|
448
|
+
'/home/u/.cache/selenium/chrome/linux64/131.0.6778.85/chrome'
|
|
449
|
+
);
|
|
450
|
+
assert.deepEqual(cft.defaults, [
|
|
451
|
+
'no-chrome-for-testing-infobar',
|
|
452
|
+
'mock-keychain',
|
|
453
|
+
]);
|
|
454
|
+
assert.deepEqual(cft.switches, ['--test-type=gpu', '--use-mock-keychain']);
|
|
455
|
+
|
|
456
|
+
const chrome = await launchWith('/opt/google/chrome/chrome');
|
|
457
|
+
assert.deepEqual(chrome, { defaults: [], switches: [] });
|
|
458
|
+
const off = await launchWith(
|
|
459
|
+
'/home/u/.cache/selenium/chrome/linux64/131.0.6778.85/chrome',
|
|
460
|
+
{ defaultRestrictions: [] }
|
|
461
|
+
);
|
|
462
|
+
assert.deepEqual(off, { defaults: [], switches: [] });
|
|
463
|
+
});
|
|
464
|
+
|
|
384
465
|
it('starts geckodriver for Firefox with a -profile argument', async () => {
|
|
385
466
|
const { mock, started, dependencies } = launchDependencies({
|
|
386
467
|
environment: { PATH: '/usr/bin' },
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
classifyStatus,
|
|
17
17
|
extractRejectedLinks,
|
|
18
18
|
judge,
|
|
19
|
+
ownRepositoryPath,
|
|
19
20
|
} from '../../../../scripts/check-web-archive.mjs';
|
|
20
21
|
|
|
21
22
|
// Verbatim shape of lychee/out.md from run 33959793880.
|
|
@@ -131,8 +132,8 @@ const waybackHas = (timestamp, archiveUrl) => async () => ({
|
|
|
131
132
|
timestamp,
|
|
132
133
|
});
|
|
133
134
|
|
|
134
|
-
const verdictFor = (link, recheckImpl, waybackImpl) =>
|
|
135
|
-
judge(link, { recheckImpl, waybackImpl });
|
|
135
|
+
const verdictFor = (link, recheckImpl, waybackImpl, extra = {}) =>
|
|
136
|
+
judge(link, { recheckImpl, waybackImpl, ...extra });
|
|
136
137
|
|
|
137
138
|
describe('judging one rejected link', () => {
|
|
138
139
|
const playwright = {
|
|
@@ -151,6 +152,14 @@ describe('judging one rejected link', () => {
|
|
|
151
152
|
assert.equal(verdict.verdict, 'transient');
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
it('does not fail for a host that answered 5xx and then timed out', async () => {
|
|
156
|
+
// chromium.googlesource.com answered 503 to lychee and then let the
|
|
157
|
+
// re-check time out: neither said the link is gone, and a host that
|
|
158
|
+
// answered at all still resolves.
|
|
159
|
+
const verdict = await verdictFor(playwright, unreachable(), waybackNever());
|
|
160
|
+
assert.equal(verdict.verdict, 'transient');
|
|
161
|
+
});
|
|
162
|
+
|
|
154
163
|
it('does not fail for a host that keeps rate-limiting the runner', async () => {
|
|
155
164
|
const verdict = await verdictFor(
|
|
156
165
|
{ url: 'https://example.com/x', status: '429' },
|
|
@@ -189,3 +198,69 @@ describe('judging one rejected link', () => {
|
|
|
189
198
|
assert.equal(verdict.verdict, 'gone');
|
|
190
199
|
});
|
|
191
200
|
});
|
|
201
|
+
|
|
202
|
+
describe('a link to a file this branch adds', () => {
|
|
203
|
+
// Run 39651456194: the READMEs link the new docs/chrome-for-testing.md on
|
|
204
|
+
// main, which is published to npm, PyPI and crates.io, so it must be an
|
|
205
|
+
// absolute URL. Until the pull request merges, main has no such file.
|
|
206
|
+
const repository = 'link-foundation/browser-commander';
|
|
207
|
+
const newDoc = {
|
|
208
|
+
url: 'https://github.com/link-foundation/browser-commander/blob/main/docs/chrome-for-testing.md',
|
|
209
|
+
status: '404',
|
|
210
|
+
};
|
|
211
|
+
const checkout = (...files) => ({
|
|
212
|
+
repository,
|
|
213
|
+
fileExists: (relative) => files.includes(relative),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('maps this repository’s main-branch links to checkout paths', () => {
|
|
217
|
+
assert.equal(
|
|
218
|
+
ownRepositoryPath(`${newDoc.url}#verifying-it`, repository),
|
|
219
|
+
'docs/chrome-for-testing.md'
|
|
220
|
+
);
|
|
221
|
+
assert.equal(
|
|
222
|
+
ownRepositoryPath(
|
|
223
|
+
'https://github.com/link-foundation/browser-commander/tree/main/docs/case%20studies',
|
|
224
|
+
repository
|
|
225
|
+
),
|
|
226
|
+
'docs/case studies'
|
|
227
|
+
);
|
|
228
|
+
assert.equal(
|
|
229
|
+
ownRepositoryPath(
|
|
230
|
+
'https://github.com/link-foundation/browser-commander/blob/v1/x.md',
|
|
231
|
+
repository
|
|
232
|
+
),
|
|
233
|
+
null
|
|
234
|
+
);
|
|
235
|
+
assert.equal(
|
|
236
|
+
ownRepositoryPath(
|
|
237
|
+
'https://github.com/other/repo/blob/main/x.md',
|
|
238
|
+
repository
|
|
239
|
+
),
|
|
240
|
+
null
|
|
241
|
+
);
|
|
242
|
+
assert.equal(ownRepositoryPath(newDoc.url, undefined), null);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('does not fail when the checkout has the file main is about to get', async () => {
|
|
246
|
+
const verdict = await verdictFor(
|
|
247
|
+
newDoc,
|
|
248
|
+
() => {
|
|
249
|
+
throw new Error('a file in the checkout needs no network request');
|
|
250
|
+
},
|
|
251
|
+
waybackNever(),
|
|
252
|
+
checkout('docs/chrome-for-testing.md')
|
|
253
|
+
);
|
|
254
|
+
assert.equal(verdict.verdict, 'unmerged');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('still fails when the checkout does not have the file either', async () => {
|
|
258
|
+
const verdict = await verdictFor(
|
|
259
|
+
newDoc,
|
|
260
|
+
answers(404),
|
|
261
|
+
waybackNothing(),
|
|
262
|
+
checkout('docs/feature-parity.md')
|
|
263
|
+
);
|
|
264
|
+
assert.equal(verdict.verdict, 'gone');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -181,7 +181,7 @@ describe('complete recording defaults (issue #162)', { timeout: 15000 }, () => {
|
|
|
181
181
|
},
|
|
182
182
|
};
|
|
183
183
|
const launched = await launchBrowserWithDependencies(
|
|
184
|
-
{ debug: { output } },
|
|
184
|
+
{ channel: 'chrome', debug: { output } },
|
|
185
185
|
{
|
|
186
186
|
resolveSystem: async () => '/usr/bin/google-chrome',
|
|
187
187
|
settleMs: 0,
|
|
@@ -244,7 +244,7 @@ describe('opt-in recording policy (issue #166)', { timeout: 15000 }, () => {
|
|
|
244
244
|
await fixture(async ({ root }) => {
|
|
245
245
|
await assert.rejects(
|
|
246
246
|
launchBrowserWithDependencies(
|
|
247
|
-
{ debug: { directory: root } },
|
|
247
|
+
{ channel: 'chrome', debug: { directory: root } },
|
|
248
248
|
{
|
|
249
249
|
resolveSystem: async () => '/usr/bin/google-chrome',
|
|
250
250
|
launchRealBrowser: async () => {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { startTrace } from '../../../src/traces/recorder.js';
|
|
6
|
+
import { TRACE_MODE } from '../../../src/traces/schema.js';
|
|
7
|
+
import { screenshot } from '../../../src/capture/index.js';
|
|
8
|
+
import {
|
|
9
|
+
createFakeCommander,
|
|
10
|
+
createFakePage,
|
|
11
|
+
useTempTraceDirectory,
|
|
12
|
+
} from '../../helpers/trace-fixtures.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A clipped engine screenshot of a headful window can leave Chrome's emulated
|
|
16
|
+
* size stuck after a resize (issue #180). Without a fixed viewport, captures
|
|
17
|
+
* go through CDP `Page.captureScreenshot` with no `clip`.
|
|
18
|
+
*/
|
|
19
|
+
function cdpFake({ viewport = null, failWith, engine = 'playwright' } = {}) {
|
|
20
|
+
const calls = { cdp: [], engine: [], detached: 0 };
|
|
21
|
+
const session = {
|
|
22
|
+
send: async (method, params) => {
|
|
23
|
+
calls.cdp.push({ method, params });
|
|
24
|
+
if (failWith) {
|
|
25
|
+
throw new Error(failWith);
|
|
26
|
+
}
|
|
27
|
+
return { data: Buffer.from('cdp-png').toString('base64') };
|
|
28
|
+
},
|
|
29
|
+
detach: async () => {
|
|
30
|
+
calls.detached += 1;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const page = createFakePage();
|
|
34
|
+
page.screenshot = async (options) => {
|
|
35
|
+
calls.engine.push(options);
|
|
36
|
+
return Buffer.from('engine-png');
|
|
37
|
+
};
|
|
38
|
+
if (engine === 'playwright') {
|
|
39
|
+
page.context = () => ({ newCDPSession: async () => session });
|
|
40
|
+
page.viewportSize = () => viewport;
|
|
41
|
+
} else {
|
|
42
|
+
page.createCDPSession = async () => session;
|
|
43
|
+
page.viewport = () => viewport;
|
|
44
|
+
}
|
|
45
|
+
return { page, calls };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('trace checkpoint screenshots (issue #180)', () => {
|
|
49
|
+
const directory = useTempTraceDirectory();
|
|
50
|
+
|
|
51
|
+
async function checkpointShot(page, { withCommander = true, engine } = {}) {
|
|
52
|
+
const commander = createFakeCommander(page, engine ? { engine } : {});
|
|
53
|
+
const trace = await startTrace({
|
|
54
|
+
page,
|
|
55
|
+
...(withCommander ? { commander } : {}),
|
|
56
|
+
output: path.join(directory.path, 'run'),
|
|
57
|
+
mode: TRACE_MODE.CHECKPOINTS,
|
|
58
|
+
network: false,
|
|
59
|
+
links: false,
|
|
60
|
+
screenshots: true,
|
|
61
|
+
});
|
|
62
|
+
await trace.checkpoint('resized');
|
|
63
|
+
const stopped = await trace.stop();
|
|
64
|
+
return (
|
|
65
|
+
await fs.readFile(path.join(stopped.path, 'checkpoints/0001.png'))
|
|
66
|
+
).toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const engine of ['playwright', 'puppeteer']) {
|
|
70
|
+
for (const withCommander of [true, false]) {
|
|
71
|
+
it(`${engine} ${withCommander ? 'commander' : 'bare page'} without a fixed viewport sends no clip`, async () => {
|
|
72
|
+
const { page, calls } = cdpFake({ engine });
|
|
73
|
+
assert.equal(
|
|
74
|
+
await checkpointShot(page, { withCommander, engine }),
|
|
75
|
+
'cdp-png'
|
|
76
|
+
);
|
|
77
|
+
assert.deepEqual(calls.cdp, [
|
|
78
|
+
{
|
|
79
|
+
method: 'Page.captureScreenshot',
|
|
80
|
+
params: {
|
|
81
|
+
format: 'png',
|
|
82
|
+
fromSurface: false,
|
|
83
|
+
captureBeyondViewport: false,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
assert.equal(calls.engine.length, 0);
|
|
88
|
+
assert.equal(calls.detached, 1);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
it('a fixed viewport keeps the engine screenshot', async () => {
|
|
94
|
+
const { page, calls } = cdpFake({
|
|
95
|
+
viewport: { width: 800, height: 600 },
|
|
96
|
+
});
|
|
97
|
+
assert.equal(await checkpointShot(page), 'engine-png');
|
|
98
|
+
assert.equal(calls.cdp.length, 0);
|
|
99
|
+
assert.deepEqual(calls.engine, [{ type: 'png' }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('falls back to the engine screenshot when the view cannot be captured', async () => {
|
|
103
|
+
const { page, calls } = cdpFake({
|
|
104
|
+
failWith: 'Unable to capture screenshot',
|
|
105
|
+
});
|
|
106
|
+
assert.equal(await checkpointShot(page), 'engine-png');
|
|
107
|
+
assert.equal(calls.cdp.length, 1);
|
|
108
|
+
assert.equal(calls.detached, 1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('a page without CDP keeps the engine screenshot', async () => {
|
|
112
|
+
const page = createFakePage();
|
|
113
|
+
assert.equal(
|
|
114
|
+
await checkpointShot(page, { withCommander: false }),
|
|
115
|
+
'fake-png'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('default commander screenshots (issue #180)', () => {
|
|
121
|
+
for (const engine of ['playwright', 'puppeteer']) {
|
|
122
|
+
it(`${engine} without a fixed viewport sends no clip`, async () => {
|
|
123
|
+
const { page, calls } = cdpFake({ engine });
|
|
124
|
+
assert.equal((await screenshot({ page, engine })).toString(), 'cdp-png');
|
|
125
|
+
assert.equal(calls.cdp.length, 1);
|
|
126
|
+
assert.equal('clip' in calls.cdp[0].params, false);
|
|
127
|
+
assert.equal(calls.engine.length, 0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it(`${engine} with a fixed viewport keeps the engine screenshot`, async () => {
|
|
131
|
+
const { page, calls } = cdpFake({
|
|
132
|
+
engine,
|
|
133
|
+
viewport: { width: 800, height: 600 },
|
|
134
|
+
});
|
|
135
|
+
assert.equal(
|
|
136
|
+
(await screenshot({ page, engine })).toString(),
|
|
137
|
+
'engine-png'
|
|
138
|
+
);
|
|
139
|
+
assert.equal(calls.cdp.length, 0);
|
|
140
|
+
assert.equal(calls.engine[0].fullPage, false);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
it('JPEG quality reaches the view capture', async () => {
|
|
145
|
+
const { page, calls } = cdpFake();
|
|
146
|
+
await screenshot({ page, format: 'jpeg', quality: 60 });
|
|
147
|
+
assert.deepEqual(calls.cdp[0].params, {
|
|
148
|
+
format: 'jpeg',
|
|
149
|
+
fromSurface: false,
|
|
150
|
+
captureBeyondViewport: false,
|
|
151
|
+
quality: 60,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
for (const options of [
|
|
156
|
+
{ fullPage: true },
|
|
157
|
+
{ clip: { x: 0, y: 0, width: 1, height: 1 } },
|
|
158
|
+
{ selector: '#pager' },
|
|
159
|
+
{ omitBackground: true },
|
|
160
|
+
{ hideScrollbars: true },
|
|
161
|
+
{ animations: 'disabled' },
|
|
162
|
+
{ scale: 'css' },
|
|
163
|
+
{ stableViewport: false },
|
|
164
|
+
]) {
|
|
165
|
+
it(`${JSON.stringify(options)} keeps the engine screenshot`, async () => {
|
|
166
|
+
const { page, calls } = cdpFake();
|
|
167
|
+
page.locator = () => ({ first: () => page });
|
|
168
|
+
await screenshot({ page, ...options });
|
|
169
|
+
assert.equal(calls.cdp.length, 0);
|
|
170
|
+
assert.equal(calls.engine.length, 1);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|