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
package/src/browser/webdriver.js
CHANGED
|
@@ -42,6 +42,10 @@ import {
|
|
|
42
42
|
} from './profile-directory.js';
|
|
43
43
|
import { buildRealBrowserArgs } from './real-browser.js';
|
|
44
44
|
import { resolveRestrictions } from './restrictions.js';
|
|
45
|
+
import {
|
|
46
|
+
defaultRestrictionsFor,
|
|
47
|
+
isChromeForTesting,
|
|
48
|
+
} from './launch-executable.js';
|
|
45
49
|
import { findBrowserSource } from './browser-sources.js';
|
|
46
50
|
import {
|
|
47
51
|
isSafariChannel,
|
|
@@ -218,7 +222,12 @@ export async function resolveWebDriverExecutable({
|
|
|
218
222
|
};
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
|
|
225
|
+
// A driver on PATH belongs to an installed browser; Chrome for Testing gets
|
|
226
|
+
// the driver of its own release from Selenium Manager (issue #179).
|
|
227
|
+
const ownDriver = isChromeForTesting(executablePath);
|
|
228
|
+
const onPath =
|
|
229
|
+
!ownDriver &&
|
|
230
|
+
(await findOnPath(name, { environment, platform, checkAccess }));
|
|
222
231
|
if (onPath) {
|
|
223
232
|
return {
|
|
224
233
|
driverPath: onPath,
|
|
@@ -227,11 +236,39 @@ export async function resolveWebDriverExecutable({
|
|
|
227
236
|
};
|
|
228
237
|
}
|
|
229
238
|
|
|
239
|
+
return await askSeleniumManager({
|
|
240
|
+
browser,
|
|
241
|
+
name,
|
|
242
|
+
executablePath,
|
|
243
|
+
ownDriver,
|
|
244
|
+
environment,
|
|
245
|
+
platform,
|
|
246
|
+
runCommand,
|
|
247
|
+
managerPath,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Ask Selenium Manager for the driver of `browser` (and the browser itself). */
|
|
252
|
+
async function askSeleniumManager({
|
|
253
|
+
browser,
|
|
254
|
+
name,
|
|
255
|
+
executablePath,
|
|
256
|
+
ownDriver,
|
|
257
|
+
environment,
|
|
258
|
+
platform,
|
|
259
|
+
runCommand,
|
|
260
|
+
managerPath,
|
|
261
|
+
}) {
|
|
230
262
|
const manager = managerPath ?? seleniumManagerPath({ platform, environment });
|
|
231
|
-
const args = [
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
263
|
+
const args = [
|
|
264
|
+
'--browser',
|
|
265
|
+
browser,
|
|
266
|
+
'--output',
|
|
267
|
+
'json',
|
|
268
|
+
...(executablePath ? ['--browser-path', executablePath] : []),
|
|
269
|
+
// Selenium Manager itself prefers a driver on PATH, whatever its release.
|
|
270
|
+
...(ownDriver ? ['--skip-driver-in-path'] : []),
|
|
271
|
+
];
|
|
235
272
|
const { stdout, code } = await runCommand(manager, args, { check: false });
|
|
236
273
|
let output;
|
|
237
274
|
try {
|
|
@@ -480,13 +517,14 @@ async function teardown({
|
|
|
480
517
|
* @param {boolean} [options.bidi=false] - Enable WebDriver BiDi (events, preload scripts, full-page screenshots)
|
|
481
518
|
* @param {string[]} [options.args] - Extra browser arguments
|
|
482
519
|
* @param {string[]} [options.restrictions] - Chrome launch restrictions (launch-restrictions.json)
|
|
520
|
+
* @param {string[]} [options.defaultRestrictions] - Restrictions that replace the browser's defaults (the 'chrome-for-testing' preset for Chrome for Testing, none otherwise); [] turns them off
|
|
483
521
|
* @param {boolean} [options.automationParity=true] - Keep navigator.webdriver false in headless Chrome
|
|
484
522
|
* @param {Object<string,string>} [options.env] - Extra environment for the driver and browser
|
|
485
523
|
* @param {number} [options.startupTimeout=30000]
|
|
486
524
|
* @param {boolean} [options.verbose=false] - Mirror the driver server's output
|
|
487
525
|
* @param {'managed'|'caller'} [options.interruption='managed'] - Who handles Ctrl+C for the driver server and the browser under it; see `startProcess`
|
|
488
526
|
* @param {Object} [dependencies] - Injected selenium modules, runCommand, startProcess, fetch, reservePort, environment, platform (tests)
|
|
489
|
-
* @returns {Promise<{driver: Object, page: Object, close: function(): Promise<void>, serverUrl: string, driverProcess: Object, driverPath: string, driverSource: string, browser: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, bidi: boolean}>}
|
|
527
|
+
* @returns {Promise<{driver: Object, page: Object, close: function(): Promise<void>, serverUrl: string, driverProcess: Object, driverPath: string, driverSource: string, browser: string, userDataDir: string, temporaryProfile: boolean, args: Array<string>, bidi: boolean, defaultRestrictions: Array<string>}>}
|
|
490
528
|
*/
|
|
491
529
|
export async function launchWebDriver(options = {}, dependencies = {}) {
|
|
492
530
|
if (!isSafariChannel(options.browser ?? 'chrome')) {
|
|
@@ -550,6 +588,17 @@ function withDriverContext(error, { serverUrl, exitCode, output }) {
|
|
|
550
588
|
);
|
|
551
589
|
}
|
|
552
590
|
|
|
591
|
+
// Chrome for Testing - the engines' download or Selenium Manager's - gets its
|
|
592
|
+
// default restrictions like any other launch of it (issue #179).
|
|
593
|
+
function launchDefaultRestrictions(
|
|
594
|
+
{ browser = 'chrome', debuggerAddress, defaultRestrictions },
|
|
595
|
+
browserPath
|
|
596
|
+
) {
|
|
597
|
+
return browser === 'chrome' && !debuggerAddress
|
|
598
|
+
? defaultRestrictionsFor(browserPath, defaultRestrictions)
|
|
599
|
+
: [];
|
|
600
|
+
}
|
|
601
|
+
|
|
553
602
|
async function launchWebDriverOnce(options, dependencies) {
|
|
554
603
|
const {
|
|
555
604
|
browser = 'chrome',
|
|
@@ -593,12 +642,17 @@ async function launchWebDriverOnce(options, dependencies) {
|
|
|
593
642
|
executablePath,
|
|
594
643
|
});
|
|
595
644
|
|
|
645
|
+
const defaultRestrictions = launchDefaultRestrictions(
|
|
646
|
+
options,
|
|
647
|
+
resolved.browserPath
|
|
648
|
+
);
|
|
649
|
+
const launchRestrictions = [...defaultRestrictions, ...restrictions];
|
|
596
650
|
const temporaryProfile = !safari && !debuggerAddress && !requestedUserDataDir;
|
|
597
651
|
const userDataDir =
|
|
598
652
|
safari || debuggerAddress
|
|
599
653
|
? undefined
|
|
600
654
|
: await resolveProfile(browser, requestedUserDataDir);
|
|
601
|
-
const childEnv = driverEnvironment(
|
|
655
|
+
const childEnv = driverEnvironment(launchRestrictions, env);
|
|
602
656
|
|
|
603
657
|
let driverProcess;
|
|
604
658
|
let driver;
|
|
@@ -636,7 +690,7 @@ async function launchWebDriverOnce(options, dependencies) {
|
|
|
636
690
|
remoteDebuggingPort:
|
|
637
691
|
browser === 'chrome' ? await reservePort() : undefined,
|
|
638
692
|
headless,
|
|
639
|
-
restrictions,
|
|
693
|
+
restrictions: launchRestrictions,
|
|
640
694
|
disableFeatures,
|
|
641
695
|
args,
|
|
642
696
|
automationParity,
|
|
@@ -675,6 +729,7 @@ async function launchWebDriverOnce(options, dependencies) {
|
|
|
675
729
|
temporaryProfile,
|
|
676
730
|
args: built.args,
|
|
677
731
|
bidi,
|
|
732
|
+
defaultRestrictions,
|
|
678
733
|
};
|
|
679
734
|
} catch (error) {
|
|
680
735
|
const exitCode = driverProcess?.exitCode;
|
|
@@ -10,6 +10,7 @@ import { captureStableViewport, validateStableViewport } from './viewport.js';
|
|
|
10
10
|
* @property {string} [path]
|
|
11
11
|
* @property {boolean} [fullPage]
|
|
12
12
|
* @property {boolean} [stableViewport] Capture only the existing view without visual styling.
|
|
13
|
+
* Default captures already do so without a fixed viewport; false keeps the engine capture.
|
|
13
14
|
* @property {string} [selector]
|
|
14
15
|
* @property {Object} [element]
|
|
15
16
|
* @property {{x:number,y:number,width:number,height:number}} [clip]
|
|
@@ -61,7 +62,7 @@ export async function screenshot(options = {}) {
|
|
|
61
62
|
if (options.waitForFonts) {
|
|
62
63
|
await page.evaluate(() => document.fonts.ready.then(() => true));
|
|
63
64
|
}
|
|
64
|
-
if (options
|
|
65
|
+
if (capturesView(options, engine, css)) {
|
|
65
66
|
if (engine === 'playwright') {
|
|
66
67
|
native.scale = scale;
|
|
67
68
|
native.caret = 'initial';
|
|
@@ -123,6 +124,20 @@ export async function screenshot(options = {}) {
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* A default capture of a page without a fixed viewport takes the view as it
|
|
129
|
+
* is: a clipped engine capture can leave a resized window stuck (issue #180).
|
|
130
|
+
*/
|
|
131
|
+
function capturesView(options, engine, css) {
|
|
132
|
+
const { selector, element, clip, fullPage, omitBackground } = options;
|
|
133
|
+
return (
|
|
134
|
+
options.stableViewport ??
|
|
135
|
+
(['playwright', 'puppeteer'].includes(engine) &&
|
|
136
|
+
![selector, element, clip, fullPage, omitBackground, css].some(Boolean) &&
|
|
137
|
+
(options.scale ?? 'device') === 'device')
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
async function finishScreenshot(
|
|
127
142
|
bytes,
|
|
128
143
|
{ page, engine, scale, format, quality, output }
|
package/src/capture/viewport.js
CHANGED
|
@@ -28,7 +28,8 @@ export async function captureStableViewport(page, engine, native) {
|
|
|
28
28
|
}
|
|
29
29
|
let session;
|
|
30
30
|
if (
|
|
31
|
-
(engine === 'playwright' &&
|
|
31
|
+
(engine === 'playwright' &&
|
|
32
|
+
typeof page.context?.()?.newCDPSession === 'function') ||
|
|
32
33
|
(engine === 'puppeteer' &&
|
|
33
34
|
(typeof page.createCDPSession === 'function' ||
|
|
34
35
|
typeof page.target === 'function'))
|
package/src/exports.js
CHANGED
|
@@ -104,6 +104,11 @@ export {
|
|
|
104
104
|
LAUNCH_MODES,
|
|
105
105
|
resolveLaunchExecutable,
|
|
106
106
|
} from './browser/launcher.js';
|
|
107
|
+
export {
|
|
108
|
+
CHROME_FOR_TESTING_RESTRICTIONS,
|
|
109
|
+
defaultRestrictionsFor,
|
|
110
|
+
isChromeForTesting,
|
|
111
|
+
} from './browser/launch-executable.js';
|
|
107
112
|
export {
|
|
108
113
|
LAUNCH_RESTRICTIONS,
|
|
109
114
|
LAUNCH_RESTRICTION_PRESETS,
|
package/src/traces/recorder.js
CHANGED
|
@@ -21,6 +21,7 @@ import { startTraceDrain } from './drain-timer.js';
|
|
|
21
21
|
import { attachRecorderObservers } from './observers.js';
|
|
22
22
|
import { attachNetwork, validateNetworkOptions } from './network.js';
|
|
23
23
|
import { startRecording } from '../capture/recording.js';
|
|
24
|
+
import { captureStableViewport } from '../capture/viewport.js';
|
|
24
25
|
import { normalizePrivacyOptions, redactValue } from './redaction.js';
|
|
25
26
|
import path from 'node:path';
|
|
26
27
|
import {
|
|
@@ -47,6 +48,17 @@ const DEFAULT_DOM_OPTIONS = Object.freeze({
|
|
|
47
48
|
openShadowRoots: true,
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
/** The CDP-capable engine a bare page belongs to, or null for any other page. */
|
|
52
|
+
function pageEngine(page) {
|
|
53
|
+
if (typeof page.context === 'function') {
|
|
54
|
+
return 'playwright';
|
|
55
|
+
}
|
|
56
|
+
return typeof page.createCDPSession === 'function' ||
|
|
57
|
+
typeof page.target === 'function'
|
|
58
|
+
? 'puppeteer'
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
function normalizeMode(mode) {
|
|
51
63
|
const values = Object.values(TRACE_MODE);
|
|
52
64
|
if (!values.includes(mode)) {
|
|
@@ -275,12 +287,16 @@ export async function startTrace(options = {}) {
|
|
|
275
287
|
return null;
|
|
276
288
|
}
|
|
277
289
|
try {
|
|
290
|
+
// A clipped engine capture can leave a resized headful window drawn at
|
|
291
|
+
// its old size (issue #180); without a fixed viewport, capture the view.
|
|
278
292
|
const shot = await withDeadline(
|
|
279
|
-
page
|
|
293
|
+
captureStableViewport(page, engine ?? pageEngine(page), {
|
|
294
|
+
type: 'png',
|
|
295
|
+
}),
|
|
280
296
|
captureTimeoutMs,
|
|
281
297
|
'trace screenshot'
|
|
282
298
|
);
|
|
283
|
-
return
|
|
299
|
+
return shot.bytes;
|
|
284
300
|
} catch (error) {
|
|
285
301
|
await bundle.drop({
|
|
286
302
|
reason: TRACE_DROP_REASON.CAPTURE_FAILED,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #179: without a channel or executablePath every engine starts Chrome
|
|
3
|
+
* for Testing, hides its "only for automated testing" infobar and keeps it
|
|
4
|
+
* away from the user's keychain.
|
|
5
|
+
*/
|
|
6
|
+
// feature-parity: launch.chrome-for-testing@native-typed
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
|
|
10
|
+
import { isChromeForTesting } from '../../src/browser/launch-executable.js';
|
|
11
|
+
import { launchBrowser } from '../../src/browser/launcher.js';
|
|
12
|
+
import { readBrowserVersionPage } from '../../src/browser/parity.js';
|
|
13
|
+
import { SANDBOX_ARGS } from '../helpers/e2e-browser.js';
|
|
14
|
+
|
|
15
|
+
function skipReason() {
|
|
16
|
+
if (!process.env.RUN_E2E) {
|
|
17
|
+
return 'set RUN_E2E=true';
|
|
18
|
+
}
|
|
19
|
+
if (process.platform === 'linux' && !process.env.DISPLAY) {
|
|
20
|
+
return 'the infobar is only drawn in a window; run under xvfb-run';
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What chrome://version reports about the browser a session started. */
|
|
26
|
+
async function versionPage(engine, session) {
|
|
27
|
+
if (engine !== 'selenium') {
|
|
28
|
+
return await readBrowserVersionPage(session.page);
|
|
29
|
+
}
|
|
30
|
+
await session.browser.get('chrome://version');
|
|
31
|
+
return await session.browser.executeScript(`
|
|
32
|
+
const text = (id) => (document.getElementById(id)?.textContent ?? '').trim();
|
|
33
|
+
return { commandLine: text('command_line'), executablePath: text('executable_path') };
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Height of the browser UI above the page: tab strip, toolbar, infobars. */
|
|
38
|
+
async function frameHeight(session) {
|
|
39
|
+
await session.page.goto('data:text/html,<title>frame</title>');
|
|
40
|
+
// The infobar appears just after the first window is shown.
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
42
|
+
return await session.page.evaluate(
|
|
43
|
+
() => globalThis.outerHeight - globalThis.innerHeight
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe(
|
|
48
|
+
'Chrome for Testing by default (issue #179)',
|
|
49
|
+
{ skip: skipReason(), timeout: 180000 },
|
|
50
|
+
() => {
|
|
51
|
+
for (const engine of ['playwright', 'puppeteer', 'selenium']) {
|
|
52
|
+
it(`${engine} starts Chrome for Testing without its infobar or keychain`, async () => {
|
|
53
|
+
const session = await launchBrowser({
|
|
54
|
+
engine,
|
|
55
|
+
headless: false,
|
|
56
|
+
args: SANDBOX_ARGS,
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
const { commandLine, executablePath } = await versionPage(
|
|
60
|
+
engine,
|
|
61
|
+
session
|
|
62
|
+
);
|
|
63
|
+
assert.equal(
|
|
64
|
+
isChromeForTesting(executablePath),
|
|
65
|
+
true,
|
|
66
|
+
executablePath
|
|
67
|
+
);
|
|
68
|
+
assert.deepEqual(session.defaultRestrictions, [
|
|
69
|
+
'no-chrome-for-testing-infobar',
|
|
70
|
+
'mock-keychain',
|
|
71
|
+
]);
|
|
72
|
+
assert.match(commandLine, /--test-type=gpu(\s|$)/u);
|
|
73
|
+
assert.match(commandLine, /--use-mock-keychain(\s|$)/u);
|
|
74
|
+
} finally {
|
|
75
|
+
await session.close();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it('the default restrictions remove the infobar the opt-out shows', async () => {
|
|
81
|
+
const heights = {};
|
|
82
|
+
for (const [name, defaultRestrictions] of [
|
|
83
|
+
['defaults', undefined],
|
|
84
|
+
['opt-out', []],
|
|
85
|
+
]) {
|
|
86
|
+
const session = await launchBrowser({
|
|
87
|
+
engine: 'playwright',
|
|
88
|
+
headless: false,
|
|
89
|
+
args: SANDBOX_ARGS,
|
|
90
|
+
...(defaultRestrictions ? { defaultRestrictions } : {}),
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
heights[name] = await frameHeight(session);
|
|
94
|
+
} finally {
|
|
95
|
+
await session.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// The infobar is about 40px tall; nothing else differs between the two.
|
|
99
|
+
assert.ok(
|
|
100
|
+
heights['opt-out'] - heights.defaults >= 20,
|
|
101
|
+
JSON.stringify(heights)
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
);
|
|
@@ -44,6 +44,7 @@ it(
|
|
|
44
44
|
async () => {
|
|
45
45
|
const sibling = await launchBrowser({
|
|
46
46
|
debug: false,
|
|
47
|
+
channel: 'chrome',
|
|
47
48
|
headless: true,
|
|
48
49
|
...CHROME_LAUNCH_OPTIONS,
|
|
49
50
|
});
|
|
@@ -58,6 +59,7 @@ it(
|
|
|
58
59
|
launchAndConnectRealBrowserWithDependencies(
|
|
59
60
|
{
|
|
60
61
|
debug: false,
|
|
62
|
+
channel: 'chrome',
|
|
61
63
|
headless: true,
|
|
62
64
|
persistSessionCookies: suppliedProfile,
|
|
63
65
|
...(suppliedProfile ? { userDataDir: root } : {}),
|
|
@@ -109,6 +109,19 @@ describe('browser hand-off (issue #174)', () => {
|
|
|
109
109
|
assert.deepEqual(await session.handoff(), handoffFor(endpoint));
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
+
it('reattaches Selenium with the driver of the handed-off browser (issue #179)', async () => {
|
|
113
|
+
const endpoint = await fakeEndpoint();
|
|
114
|
+
const cft =
|
|
115
|
+
'/home/u/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome';
|
|
116
|
+
const calls = [];
|
|
117
|
+
await reconnectBrowserWithDependencies(
|
|
118
|
+
handoffFor(endpoint, { executablePath: cft }),
|
|
119
|
+
{},
|
|
120
|
+
{ connect: fakeConnect(calls) }
|
|
121
|
+
);
|
|
122
|
+
assert.equal(calls[0][1].executablePath, cft);
|
|
123
|
+
});
|
|
124
|
+
|
|
112
125
|
it('reads a hand-off from a JSON file', async () => {
|
|
113
126
|
const endpoint = await fakeEndpoint();
|
|
114
127
|
const directory = await mkdtemp(path.join(os.tmpdir(), 'bc-handoff-'));
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { after, describe, it } from 'node:test';
|
|
6
|
+
import {
|
|
7
|
+
cacheLocations,
|
|
8
|
+
cachedChromeForTesting,
|
|
9
|
+
chromeForTestingCandidates,
|
|
10
|
+
} from '../../../src/browser/chrome-for-testing-cache.js';
|
|
11
|
+
import { isChromeForTesting } from '../../../src/browser/launch-executable.js';
|
|
12
|
+
|
|
13
|
+
const home = await mkdtemp(path.join(tmpdir(), 'browser-commander-cache-'));
|
|
14
|
+
after(() => rm(home, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
async function install(...segments) {
|
|
17
|
+
const file = path.join(home, ...segments);
|
|
18
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
19
|
+
await writeFile(file, '#!/bin/sh\n');
|
|
20
|
+
await chmod(file, 0o755);
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const linux = (env = {}) =>
|
|
25
|
+
cacheLocations({ platform: 'linux', arch: 'x64', home, env });
|
|
26
|
+
|
|
27
|
+
describe('Chrome for Testing in the download caches (issue #179)', () => {
|
|
28
|
+
it('lists the Playwright builds newest revision first', async () => {
|
|
29
|
+
const old = await install(
|
|
30
|
+
'.cache/ms-playwright/chromium-999/chrome-linux64/chrome'
|
|
31
|
+
);
|
|
32
|
+
const recent = await install(
|
|
33
|
+
'.cache/ms-playwright/chromium-1248/chrome-linux64/chrome'
|
|
34
|
+
);
|
|
35
|
+
await mkdir(
|
|
36
|
+
path.join(home, '.cache/ms-playwright/chromium_headless_shell-1300'),
|
|
37
|
+
{
|
|
38
|
+
recursive: true,
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
assert.deepEqual(chromeForTestingCandidates('playwright', linux()), [
|
|
42
|
+
recent,
|
|
43
|
+
old,
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('follows PLAYWRIGHT_BROWSERS_PATH and XDG_CACHE_HOME', async () => {
|
|
48
|
+
const moved = await install('moved/chromium-1300/chrome-linux64/chrome');
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
chromeForTestingCandidates(
|
|
51
|
+
'playwright',
|
|
52
|
+
linux({ PLAYWRIGHT_BROWSERS_PATH: path.join(home, 'moved') })
|
|
53
|
+
),
|
|
54
|
+
[moved]
|
|
55
|
+
);
|
|
56
|
+
assert.deepEqual(
|
|
57
|
+
chromeForTestingCandidates(
|
|
58
|
+
'playwright',
|
|
59
|
+
linux({ PLAYWRIGHT_BROWSERS_PATH: '0' })
|
|
60
|
+
),
|
|
61
|
+
[]
|
|
62
|
+
);
|
|
63
|
+
const xdg = await install(
|
|
64
|
+
'xdg/ms-playwright/chromium-1/chrome-linux64/chrome'
|
|
65
|
+
);
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
chromeForTestingCandidates(
|
|
68
|
+
'playwright',
|
|
69
|
+
linux({ XDG_CACHE_HOME: path.join(home, 'xdg') })
|
|
70
|
+
),
|
|
71
|
+
[xdg]
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('compares Puppeteer and Selenium versions numerically', async () => {
|
|
76
|
+
const newer = await install(
|
|
77
|
+
'.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome'
|
|
78
|
+
);
|
|
79
|
+
const older = await install(
|
|
80
|
+
'.cache/puppeteer/chrome/linux-99.0.1.1/chrome-linux64/chrome'
|
|
81
|
+
);
|
|
82
|
+
assert.deepEqual(chromeForTestingCandidates('puppeteer', linux()), [
|
|
83
|
+
newer,
|
|
84
|
+
older,
|
|
85
|
+
]);
|
|
86
|
+
const selenium = await install(
|
|
87
|
+
'.cache/selenium/chrome/linux64/131.0.6778.85/chrome'
|
|
88
|
+
);
|
|
89
|
+
assert.deepEqual(chromeForTestingCandidates('selenium', linux()), [
|
|
90
|
+
selenium,
|
|
91
|
+
]);
|
|
92
|
+
for (const file of [newer, older, selenium]) {
|
|
93
|
+
assert.ok(isChromeForTesting(file), file);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('searches the caches each engine uses, in order', () => {
|
|
98
|
+
const machine = { platform: 'linux', arch: 'x64', home, env: {} };
|
|
99
|
+
const cacheOf = (file) =>
|
|
100
|
+
['ms-playwright', 'puppeteer', 'selenium'].find((cache) =>
|
|
101
|
+
file.split(path.sep).includes(cache)
|
|
102
|
+
);
|
|
103
|
+
const caches = (engine) => [
|
|
104
|
+
...new Set(cachedChromeForTesting(engine, machine).map(cacheOf)),
|
|
105
|
+
];
|
|
106
|
+
assert.deepEqual(caches('playwright'), ['ms-playwright']);
|
|
107
|
+
assert.deepEqual(caches('puppeteer'), ['puppeteer']);
|
|
108
|
+
assert.deepEqual(caches('selenium'), [
|
|
109
|
+
'ms-playwright',
|
|
110
|
+
'puppeteer',
|
|
111
|
+
'selenium',
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('knows the macOS and Windows layouts and skips other platforms', () => {
|
|
116
|
+
assert.equal(
|
|
117
|
+
cacheLocations({ platform: 'darwin', arch: 'arm64', home, env: {} }).name,
|
|
118
|
+
'mac-arm64'
|
|
119
|
+
);
|
|
120
|
+
assert.equal(
|
|
121
|
+
cacheLocations({ platform: 'win32', arch: 'ia32', home, env: {} })
|
|
122
|
+
.puppeteer,
|
|
123
|
+
'win32'
|
|
124
|
+
);
|
|
125
|
+
assert.equal(
|
|
126
|
+
cacheLocations({ platform: 'linux', arch: 'arm64', home, env: {} }),
|
|
127
|
+
undefined
|
|
128
|
+
);
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
cachedChromeForTesting('playwright', {
|
|
131
|
+
platform: 'linux',
|
|
132
|
+
arch: 'arm64',
|
|
133
|
+
home,
|
|
134
|
+
env: {},
|
|
135
|
+
}),
|
|
136
|
+
[]
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// feature-parity: launch.chrome-for-testing@native-typed
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CHROME_FOR_TESTING_RESTRICTIONS,
|
|
7
|
+
defaultRestrictionsFor,
|
|
8
|
+
isChromeForTesting,
|
|
9
|
+
} from '../../../src/browser/launch-executable.js';
|
|
10
|
+
import { resolveRestrictions } from '../../../src/browser/restrictions.js';
|
|
11
|
+
import * as api from '../../../src/index.js';
|
|
12
|
+
|
|
13
|
+
describe('isChromeForTesting (issue #179)', () => {
|
|
14
|
+
it('recognises the Chrome for Testing install layouts', () => {
|
|
15
|
+
for (const executable of [
|
|
16
|
+
// Playwright
|
|
17
|
+
'/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome',
|
|
18
|
+
'/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
19
|
+
'C:\\Users\\u\\AppData\\Local\\ms-playwright\\chromium-1248\\chrome-win64\\chrome.exe',
|
|
20
|
+
// Puppeteer and @puppeteer/browsers
|
|
21
|
+
'/home/u/.cache/puppeteer/chrome/linux-131.0.6778.85/chrome-linux64/chrome',
|
|
22
|
+
'/Users/u/.cache/puppeteer/chrome/mac_arm-131.0.6778.85/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
23
|
+
'C:\\Users\\u\\.cache\\puppeteer\\chrome\\win32-131.0.6778.85\\chrome-win32\\chrome.exe',
|
|
24
|
+
'/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
25
|
+
// Selenium Manager
|
|
26
|
+
'/home/u/.cache/selenium/chrome/linux64/131.0.6778.85/chrome',
|
|
27
|
+
'C:\\Users\\u\\.cache\\selenium\\chrome\\win64\\131.0.6778.85\\chrome.exe',
|
|
28
|
+
// An app copied anywhere keeps its name
|
|
29
|
+
'/Applications/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
30
|
+
]) {
|
|
31
|
+
assert.equal(isChromeForTesting(executable), true, executable);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('leaves every other browser alone', () => {
|
|
36
|
+
for (const executable of [
|
|
37
|
+
undefined,
|
|
38
|
+
'',
|
|
39
|
+
'/usr/bin/google-chrome',
|
|
40
|
+
'/opt/google/chrome/chrome',
|
|
41
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
42
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
43
|
+
'/usr/bin/chromium',
|
|
44
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
45
|
+
// Playwright's own Chromium builds before Chrome for Testing
|
|
46
|
+
'/home/u/.cache/ms-playwright/chromium-1100/chrome-linux/chrome',
|
|
47
|
+
'/Users/u/Library/Caches/ms-playwright/chromium-1100/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
|
48
|
+
// Selenium Manager's Firefox cache
|
|
49
|
+
'/home/u/.cache/selenium/firefox/linux64/133.0/firefox',
|
|
50
|
+
]) {
|
|
51
|
+
assert.equal(isChromeForTesting(executable), false, String(executable));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('defaultRestrictionsFor (issue #179)', () => {
|
|
57
|
+
const cft =
|
|
58
|
+
'/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
|
|
59
|
+
|
|
60
|
+
it('gives Chrome for Testing the chrome-for-testing preset', () => {
|
|
61
|
+
assert.deepEqual(defaultRestrictionsFor(cft), [
|
|
62
|
+
'no-chrome-for-testing-infobar',
|
|
63
|
+
'mock-keychain',
|
|
64
|
+
]);
|
|
65
|
+
assert.deepEqual(defaultRestrictionsFor('/usr/bin/google-chrome'), []);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("uses the caller's defaultRestrictions as given", () => {
|
|
69
|
+
assert.deepEqual(defaultRestrictionsFor(cft, []), []);
|
|
70
|
+
assert.deepEqual(
|
|
71
|
+
defaultRestrictionsFor('/usr/bin/google-chrome', ['mock-keychain']),
|
|
72
|
+
['mock-keychain']
|
|
73
|
+
);
|
|
74
|
+
// The preset itself is never handed out to be mutated.
|
|
75
|
+
defaultRestrictionsFor(cft).push('no-sync');
|
|
76
|
+
assert.deepEqual(CHROME_FOR_TESTING_RESTRICTIONS, [
|
|
77
|
+
'no-chrome-for-testing-infobar',
|
|
78
|
+
'mock-keychain',
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('hides the infobar with --test-type=gpu and skips the keychain prompt', () => {
|
|
83
|
+
assert.deepEqual(resolveRestrictions(['chrome-for-testing']).args, [
|
|
84
|
+
'--test-type=gpu',
|
|
85
|
+
'--use-mock-keychain',
|
|
86
|
+
]);
|
|
87
|
+
assert.deepEqual(api.LAUNCH_RESTRICTION_PRESETS['chrome-for-testing'], [
|
|
88
|
+
'no-chrome-for-testing-infobar',
|
|
89
|
+
'mock-keychain',
|
|
90
|
+
]);
|
|
91
|
+
assert.equal(api.isChromeForTesting, isChromeForTesting);
|
|
92
|
+
assert.equal(api.defaultRestrictionsFor, defaultRestrictionsFor);
|
|
93
|
+
});
|
|
94
|
+
});
|