browser-commander 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +25 -6
- package/scripts/run-tests.mjs +4 -2
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/downloads/manager.js +9 -0
- package/src/downloads/sources.js +71 -0
- package/src/downloads/staging.js +21 -9
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/traces/bundle.js +10 -2
- package/src/traces/recorder.js +16 -3
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/downloads.e2e.test.js +79 -2
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/trace-fixtures.js +18 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/downloads/manager.test.js +82 -9
- package/tests/unit/downloads/staging.test.js +24 -2
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +7 -0
- package/tests/unit/traces/reader.test.js +11 -4
- package/tests/unit/traces/recorder.test.js +4 -0
- package/tests/unit/traces/viewer.test.js +4 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createCdpSession,
|
|
6
|
+
wrapCdpSession,
|
|
7
|
+
} from '../../../src/browser/cdp-session.js';
|
|
8
|
+
import { makeBrowserCommander } from '../../../src/factory.js';
|
|
9
|
+
import * as publicApi from '../../../src/index.js';
|
|
10
|
+
|
|
11
|
+
function createEngineSession() {
|
|
12
|
+
const calls = [];
|
|
13
|
+
const listeners = [];
|
|
14
|
+
return {
|
|
15
|
+
calls,
|
|
16
|
+
listeners,
|
|
17
|
+
detachCount: 0,
|
|
18
|
+
send(method, params) {
|
|
19
|
+
calls.push({ method, params });
|
|
20
|
+
return Promise.resolve({ echoed: method });
|
|
21
|
+
},
|
|
22
|
+
on(event, listener) {
|
|
23
|
+
listeners.push(['on', event, listener]);
|
|
24
|
+
},
|
|
25
|
+
once(event, listener) {
|
|
26
|
+
listeners.push(['once', event, listener]);
|
|
27
|
+
},
|
|
28
|
+
off(event, listener) {
|
|
29
|
+
listeners.push(['off', event, listener]);
|
|
30
|
+
},
|
|
31
|
+
detach() {
|
|
32
|
+
this.detachCount += 1;
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createPlaywrightPage(session) {
|
|
39
|
+
const context = {
|
|
40
|
+
targets: [],
|
|
41
|
+
newCDPSession(target) {
|
|
42
|
+
context.targets.push(target);
|
|
43
|
+
return Promise.resolve(session);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const page = { context: () => context, locator: () => ({}) };
|
|
47
|
+
return { page, context };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('createCdpSession', () => {
|
|
51
|
+
it('opens a Playwright session through page.context().newCDPSession(page)', async () => {
|
|
52
|
+
const session = createEngineSession();
|
|
53
|
+
const { page, context } = createPlaywrightPage(session);
|
|
54
|
+
|
|
55
|
+
const cdp = await createCdpSession(page, { engine: 'playwright' });
|
|
56
|
+
|
|
57
|
+
assert.equal(cdp.engine, 'playwright');
|
|
58
|
+
assert.equal(cdp.session, session);
|
|
59
|
+
assert.deepEqual(context.targets, [page]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('opens a Puppeteer session through page.createCDPSession()', async () => {
|
|
63
|
+
const session = createEngineSession();
|
|
64
|
+
const page = { createCDPSession: () => Promise.resolve(session) };
|
|
65
|
+
|
|
66
|
+
const cdp = await createCdpSession(page, { engine: 'puppeteer' });
|
|
67
|
+
|
|
68
|
+
assert.equal(cdp.engine, 'puppeteer');
|
|
69
|
+
assert.equal(cdp.session, session);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('detects the engine from the page when it is not given', async () => {
|
|
73
|
+
const session = createEngineSession();
|
|
74
|
+
const { page } = createPlaywrightPage(session);
|
|
75
|
+
|
|
76
|
+
assert.equal((await createCdpSession(page)).engine, 'playwright');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('accepts the older {browser, page, engine} form', async () => {
|
|
80
|
+
const session = createEngineSession();
|
|
81
|
+
const page = { createCDPSession: () => Promise.resolve(session) };
|
|
82
|
+
|
|
83
|
+
const cdp = await createCdpSession({
|
|
84
|
+
browser: {},
|
|
85
|
+
page,
|
|
86
|
+
engine: 'puppeteer',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.equal(cdp.session, session);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('requires a page', async () => {
|
|
93
|
+
await assert.rejects(() => createCdpSession(undefined), /requires a page/u);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('the uniform CDP session surface', () => {
|
|
98
|
+
it('sends commands with empty params by default', async () => {
|
|
99
|
+
const session = createEngineSession();
|
|
100
|
+
const cdp = wrapCdpSession(session, 'puppeteer');
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(await cdp.send('Browser.getVersion'), {
|
|
103
|
+
echoed: 'Browser.getVersion',
|
|
104
|
+
});
|
|
105
|
+
await cdp.send('Runtime.evaluate', { expression: '1' });
|
|
106
|
+
assert.deepEqual(session.calls, [
|
|
107
|
+
{ method: 'Browser.getVersion', params: {} },
|
|
108
|
+
{ method: 'Runtime.evaluate', params: { expression: '1' } },
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('forwards listeners and chains', () => {
|
|
113
|
+
const session = createEngineSession();
|
|
114
|
+
const cdp = wrapCdpSession(session, 'playwright');
|
|
115
|
+
const listener = () => {};
|
|
116
|
+
|
|
117
|
+
assert.equal(cdp.on('Network.requestWillBeSent', listener), cdp);
|
|
118
|
+
assert.equal(cdp.once('Page.loadEventFired', listener), cdp);
|
|
119
|
+
assert.equal(cdp.off('Network.requestWillBeSent', listener), cdp);
|
|
120
|
+
assert.deepEqual(
|
|
121
|
+
session.listeners.map(([kind, event]) => `${kind}:${event}`),
|
|
122
|
+
[
|
|
123
|
+
'on:Network.requestWillBeSent',
|
|
124
|
+
'once:Page.loadEventFired',
|
|
125
|
+
'off:Network.requestWillBeSent',
|
|
126
|
+
]
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('detaches once', async () => {
|
|
131
|
+
const session = createEngineSession();
|
|
132
|
+
const cdp = wrapCdpSession(session, 'playwright');
|
|
133
|
+
|
|
134
|
+
await cdp.detach();
|
|
135
|
+
await cdp.detach();
|
|
136
|
+
|
|
137
|
+
assert.equal(session.detachCount, 1);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('public cdpSession entry points', () => {
|
|
142
|
+
it('exports createCdpSession and wrapCdpSession', () => {
|
|
143
|
+
assert.equal(publicApi.createCdpSession, createCdpSession);
|
|
144
|
+
assert.equal(publicApi.wrapCdpSession, wrapCdpSession);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('opens a session from a commander', async () => {
|
|
148
|
+
const session = createEngineSession();
|
|
149
|
+
const { page } = createPlaywrightPage(session);
|
|
150
|
+
const commander = makeBrowserCommander({
|
|
151
|
+
page,
|
|
152
|
+
enableNetworkTracking: false,
|
|
153
|
+
enableNavigationManager: false,
|
|
154
|
+
enableDialogManager: false,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const cdp = await commander.createCdpSession();
|
|
158
|
+
|
|
159
|
+
assert.equal(cdp.engine, 'playwright');
|
|
160
|
+
assert.equal(cdp.session, session);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -3,6 +3,7 @@ import { describe, it } from 'node:test';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
connectBrowser,
|
|
6
|
+
pickForegroundPage,
|
|
6
7
|
connectBrowserWithDependencies,
|
|
7
8
|
} from '../../../src/browser/connector.js';
|
|
8
9
|
import { connectBrowser as publicConnectBrowser } from '../../../src/index.js';
|
|
@@ -127,3 +128,25 @@ describe('connectBrowser', () => {
|
|
|
127
128
|
);
|
|
128
129
|
});
|
|
129
130
|
});
|
|
131
|
+
|
|
132
|
+
describe('pickForegroundPage', () => {
|
|
133
|
+
const page = (id, state) => ({ id, evaluate: async () => state });
|
|
134
|
+
|
|
135
|
+
it('prefers the visible tab over the first one', async () => {
|
|
136
|
+
const picked = await pickForegroundPage([
|
|
137
|
+
page('ntp', 'hidden'),
|
|
138
|
+
page('whats-new', 'visible'),
|
|
139
|
+
]);
|
|
140
|
+
assert.equal(picked.id, 'whats-new');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('falls back to the first tab when none reports visible', async () => {
|
|
144
|
+
const failing = {
|
|
145
|
+
id: 'crashed',
|
|
146
|
+
evaluate: async () => Promise.reject(new Error('gone')),
|
|
147
|
+
};
|
|
148
|
+
const picked = await pickForegroundPage([failing, page('b', 'hidden')]);
|
|
149
|
+
assert.equal(picked.id, 'crashed');
|
|
150
|
+
assert.equal(await pickForegroundPage([]), undefined);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { PassThrough } from 'node:stream';
|
|
7
|
+
import {
|
|
8
|
+
access,
|
|
9
|
+
mkdtemp,
|
|
10
|
+
readFile,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from 'node:fs/promises';
|
|
15
|
+
import {
|
|
16
|
+
LAUNCH_RESTRICTIONS,
|
|
17
|
+
LAUNCH_RESTRICTION_PRESETS,
|
|
18
|
+
mergeFeatureSwitches,
|
|
19
|
+
resolveRestrictions,
|
|
20
|
+
} from '../../../src/browser/restrictions.js';
|
|
21
|
+
import {
|
|
22
|
+
FIRST_RUN_SENTINEL,
|
|
23
|
+
INITIAL_LOCAL_STATE,
|
|
24
|
+
LOCAL_STATE_FILE,
|
|
25
|
+
TEMPORARY_PROFILE_PREFIX,
|
|
26
|
+
createTemporaryUserDataDir,
|
|
27
|
+
prepareUserDataDir,
|
|
28
|
+
removeUserDataDir,
|
|
29
|
+
} from '../../../src/browser/profile-directory.js';
|
|
30
|
+
import {
|
|
31
|
+
PortRaceError,
|
|
32
|
+
assertFixedDebuggingPort,
|
|
33
|
+
classifyDevToolsOwnership,
|
|
34
|
+
parseDevToolsOutput,
|
|
35
|
+
reserveLoopbackPort,
|
|
36
|
+
watchDevToolsOutput,
|
|
37
|
+
} from '../../../src/browser/debugging-port.js';
|
|
38
|
+
import { CHROME_ARGS } from '../../../src/core/constants.js';
|
|
39
|
+
import { detectAutomationControlledTriggers } from '../../../src/fingerprint/automation-parity.js';
|
|
40
|
+
|
|
41
|
+
describe('launch restrictions', () => {
|
|
42
|
+
it('has unique ids and a description for each entry', () => {
|
|
43
|
+
const ids = LAUNCH_RESTRICTIONS.map((entry) => entry.id);
|
|
44
|
+
assert.equal(new Set(ids).size, ids.length);
|
|
45
|
+
for (const entry of LAUNCH_RESTRICTIONS) {
|
|
46
|
+
assert.ok(entry.description.length > 10, entry.id);
|
|
47
|
+
assert.ok(entry.args || entry.disableFeatures || entry.env, entry.id);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('never offers a switch that turns AutomationControlled on', () => {
|
|
52
|
+
for (const entry of LAUNCH_RESTRICTIONS) {
|
|
53
|
+
assert.deepEqual(
|
|
54
|
+
detectAutomationControlledTriggers(entry.args ?? []),
|
|
55
|
+
[],
|
|
56
|
+
entry.id
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('keeps the pre-#103 defaults available as a preset', () => {
|
|
62
|
+
const { args } = resolveRestrictions(['legacy-defaults']);
|
|
63
|
+
assert.deepEqual([...args].sort(), [...CHROME_ARGS].sort());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('adds the Google key override only to the browser environment', () => {
|
|
67
|
+
const resolved = resolveRestrictions(['legacy-launch-browser']);
|
|
68
|
+
assert.deepEqual(resolved.env, {
|
|
69
|
+
GOOGLE_API_KEY: 'no',
|
|
70
|
+
GOOGLE_DEFAULT_CLIENT_ID: 'no',
|
|
71
|
+
GOOGLE_DEFAULT_CLIENT_SECRET: 'no',
|
|
72
|
+
});
|
|
73
|
+
assert.ok(resolved.args.includes('--disable-features=Translate'));
|
|
74
|
+
assert.ok(LAUNCH_RESTRICTION_PRESETS['legacy-launch-browser']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('deduplicates ids and joins feature lists into one switch', () => {
|
|
78
|
+
const resolved = resolveRestrictions([
|
|
79
|
+
'no-translate',
|
|
80
|
+
'no-lens',
|
|
81
|
+
'no-translate',
|
|
82
|
+
]);
|
|
83
|
+
assert.deepEqual(resolved.ids, ['no-translate', 'no-lens']);
|
|
84
|
+
assert.deepEqual(resolved.args, [
|
|
85
|
+
'--disable-features=Translate,LensOverlay',
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects unknown names and non-arrays', () => {
|
|
90
|
+
assert.throws(() => resolveRestrictions(['nope']), RangeError);
|
|
91
|
+
assert.throws(() => resolveRestrictions('no-sync'), TypeError);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('merges repeated feature switches into the first position', () => {
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
mergeFeatureSwitches([
|
|
97
|
+
'--a',
|
|
98
|
+
'--disable-features=A,B',
|
|
99
|
+
'--enable-features=X',
|
|
100
|
+
'--disable-features=B,C',
|
|
101
|
+
'--b',
|
|
102
|
+
]),
|
|
103
|
+
['--a', '--disable-features=A,B,C', '--enable-features=X', '--b']
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('profile directory', () => {
|
|
109
|
+
it('creates a fresh profile with the First Run sentinel', async () => {
|
|
110
|
+
const dir = await createTemporaryUserDataDir();
|
|
111
|
+
try {
|
|
112
|
+
assert.ok(path.basename(dir).startsWith(TEMPORARY_PROFILE_PREFIX));
|
|
113
|
+
await access(path.join(dir, FIRST_RUN_SENTINEL));
|
|
114
|
+
assert.deepEqual(
|
|
115
|
+
JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8')),
|
|
116
|
+
INITIAL_LOCAL_STATE
|
|
117
|
+
);
|
|
118
|
+
} finally {
|
|
119
|
+
await removeUserDataDir(dir);
|
|
120
|
+
}
|
|
121
|
+
await assert.rejects(() => access(dir), /ENOENT/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('never overwrites Local State that Chrome already wrote', async () => {
|
|
125
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
126
|
+
try {
|
|
127
|
+
const localState = path.join(dir, LOCAL_STATE_FILE);
|
|
128
|
+
await writeFile(
|
|
129
|
+
localState,
|
|
130
|
+
'{"browser":{"enabled_labs_experiments":[]}}'
|
|
131
|
+
);
|
|
132
|
+
await prepareUserDataDir(dir);
|
|
133
|
+
assert.equal(
|
|
134
|
+
await readFile(localState, 'utf8'),
|
|
135
|
+
'{"browser":{"enabled_labs_experiments":[]}}'
|
|
136
|
+
);
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('keeps an existing sentinel untouched', async () => {
|
|
143
|
+
const parent = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
|
|
144
|
+
try {
|
|
145
|
+
const dir = path.join(parent, 'nested', 'profile');
|
|
146
|
+
await prepareUserDataDir(dir);
|
|
147
|
+
const sentinel = path.join(dir, FIRST_RUN_SENTINEL);
|
|
148
|
+
const before = await stat(sentinel);
|
|
149
|
+
await prepareUserDataDir(dir);
|
|
150
|
+
assert.equal((await stat(sentinel)).mtimeMs, before.mtimeMs);
|
|
151
|
+
assert.equal(await readFile(sentinel, 'utf8'), '');
|
|
152
|
+
} finally {
|
|
153
|
+
await rm(parent, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('debugging port', () => {
|
|
159
|
+
it('reserves a loopback port that can be bound again', async () => {
|
|
160
|
+
const port = await reserveLoopbackPort();
|
|
161
|
+
assert.ok(port > 0 && port < 65_536);
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const server = net.createServer();
|
|
164
|
+
server.once('error', reject);
|
|
165
|
+
server.listen({ port, host: '127.0.0.1' }, () => server.close(resolve));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('refuses port 0 and out-of-range ports', () => {
|
|
170
|
+
assert.throws(() => assertFixedDebuggingPort(0), /AutomationControlled/);
|
|
171
|
+
assert.throws(() => assertFixedDebuggingPort(70_000), RangeError);
|
|
172
|
+
assert.throws(() => assertFixedDebuggingPort('9222'), RangeError);
|
|
173
|
+
assert.equal(assertFixedDebuggingPort(9222), 9222);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('parses the DevTools listening line', () => {
|
|
177
|
+
assert.deepEqual(
|
|
178
|
+
parseDevToolsOutput(
|
|
179
|
+
'\nDevTools listening on ws://127.0.0.1:9222/devtools/browser/1-2\n'
|
|
180
|
+
),
|
|
181
|
+
{
|
|
182
|
+
listening: {
|
|
183
|
+
url: 'ws://127.0.0.1:9222/devtools/browser/1-2',
|
|
184
|
+
host: '127.0.0.1',
|
|
185
|
+
port: 9222,
|
|
186
|
+
},
|
|
187
|
+
bindFailed: false,
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
assert.equal(
|
|
191
|
+
parseDevToolsOutput(
|
|
192
|
+
'DevTools listening on ws://[::1]:9222/devtools/browser/x'
|
|
193
|
+
).listening.host,
|
|
194
|
+
'::1'
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('classifies ownership, fallback and give-up output', () => {
|
|
199
|
+
const owned = parseDevToolsOutput(
|
|
200
|
+
'DevTools listening on ws://127.0.0.1:9222/devtools/browser/x'
|
|
201
|
+
);
|
|
202
|
+
assert.equal(classifyDevToolsOwnership(owned, 9222), 'owned');
|
|
203
|
+
assert.equal(classifyDevToolsOwnership(owned, 9223), 'race');
|
|
204
|
+
const fallback = parseDevToolsOutput(
|
|
205
|
+
'bind() failed\nDevTools listening on ws://[::1]:9222/devtools/browser/x'
|
|
206
|
+
);
|
|
207
|
+
assert.equal(classifyDevToolsOwnership(fallback, 9222), 'race');
|
|
208
|
+
assert.equal(
|
|
209
|
+
classifyDevToolsOwnership(
|
|
210
|
+
parseDevToolsOutput('Cannot start http server for devtools.'),
|
|
211
|
+
9222
|
|
212
|
+
),
|
|
213
|
+
'race'
|
|
214
|
+
);
|
|
215
|
+
// Unrelated bind failures (mDNS, media router) are not conclusive.
|
|
216
|
+
assert.equal(
|
|
217
|
+
classifyDevToolsOwnership(parseDevToolsOutput('bind() failed'), 9222),
|
|
218
|
+
'pending'
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('watches a stream across chunk boundaries', () => {
|
|
223
|
+
const stream = new PassThrough();
|
|
224
|
+
const watcher = watchDevToolsOutput(stream);
|
|
225
|
+
assert.equal(watcher.available, true);
|
|
226
|
+
stream.emit('data', 'DevTools listening on ws://127.0.0.1:92');
|
|
227
|
+
assert.equal(watcher.state().listening, null);
|
|
228
|
+
stream.emit('data', '22/devtools/browser/abc\n');
|
|
229
|
+
assert.equal(watcher.state().listening.port, 9222);
|
|
230
|
+
assert.equal(watchDevToolsOutput(undefined).available, false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('names the port in race errors', () => {
|
|
234
|
+
const error = new PortRaceError(9222, 'bind failed');
|
|
235
|
+
assert.equal(error.port, 9222);
|
|
236
|
+
assert.match(error.message, /9222.*bind failed/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -6,45 +6,64 @@ import {
|
|
|
6
6
|
buildPuppeteerLaunchOptions,
|
|
7
7
|
resolveChromeArgs,
|
|
8
8
|
} from '../../../src/browser/launch-options.js';
|
|
9
|
+
import { CHROME_ARGS } from '../../../src/core/constants.js';
|
|
9
10
|
|
|
10
11
|
describe('browser launch options', () => {
|
|
11
|
-
it('
|
|
12
|
-
const options = resolveChromeArgs(
|
|
13
|
-
args: ['--legacy-arg'],
|
|
14
|
-
extraArgs: ['--lang=en-US'],
|
|
15
|
-
ignoreDefaultArgs: ['--no-default-browser-check'],
|
|
16
|
-
});
|
|
12
|
+
it('adds no switches of its own by default (issue #103)', () => {
|
|
13
|
+
const options = resolveChromeArgs();
|
|
17
14
|
|
|
18
|
-
assert.
|
|
19
|
-
assert.
|
|
20
|
-
assert.equal(options.args.includes('--no-default-browser-check'), false);
|
|
21
|
-
assert.deepEqual(options.args.slice(-2), ['--legacy-arg', '--lang=en-US']);
|
|
22
|
-
assert.deepEqual(options.ignoreDefaultArgs, ['--no-default-browser-check']);
|
|
15
|
+
assert.deepEqual(options.args, []);
|
|
16
|
+
assert.deepEqual(options.ignoreDefaultArgs, []);
|
|
23
17
|
});
|
|
24
18
|
|
|
25
|
-
it('
|
|
19
|
+
it('appends custom args after opt-in restrictions', () => {
|
|
26
20
|
const options = resolveChromeArgs({
|
|
21
|
+
restrictions: ['no-sync', 'no-translate'],
|
|
22
|
+
args: ['--legacy-arg', '--disable-features=Foo'],
|
|
27
23
|
extraArgs: ['--lang=en-US'],
|
|
28
|
-
ignoreDefaultArgs: true,
|
|
29
24
|
});
|
|
30
25
|
|
|
31
|
-
assert.deepEqual(options.args, [
|
|
32
|
-
|
|
26
|
+
assert.deepEqual(options.args, [
|
|
27
|
+
'--disable-sync',
|
|
28
|
+
// Chrome keeps only the last --disable-features, so they are merged.
|
|
29
|
+
'--disable-features=Translate,Foo',
|
|
30
|
+
'--legacy-arg',
|
|
31
|
+
'--lang=en-US',
|
|
32
|
+
]);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it('
|
|
36
|
-
const options = resolveChromeArgs({
|
|
37
|
-
|
|
35
|
+
it('restores the pre-#103 switches through the legacy-defaults preset', () => {
|
|
36
|
+
const options = resolveChromeArgs({ restrictions: ['legacy-defaults'] });
|
|
37
|
+
|
|
38
|
+
assert.deepEqual([...options.args].sort(), [...CHROME_ARGS].sort());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects malformed arguments and unknown restrictions', () => {
|
|
42
|
+
assert.throws(() => resolveChromeArgs({ args: '--x' }), TypeError);
|
|
43
|
+
assert.throws(
|
|
44
|
+
() => resolveChromeArgs({ ignoreDefaultArgs: 'yes' }),
|
|
45
|
+
TypeError
|
|
46
|
+
);
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => resolveChromeArgs({ restrictions: ['no-such-thing'] }),
|
|
49
|
+
RangeError
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('never adds --start-maximized to Puppeteer', () => {
|
|
54
|
+
const options = buildPuppeteerLaunchOptions({
|
|
55
|
+
headless: false,
|
|
56
|
+
chromeArgs: [],
|
|
57
|
+
userDataDir: '/tmp/browser-commander-test',
|
|
38
58
|
});
|
|
39
59
|
|
|
40
|
-
assert.equal(options.args.includes('--
|
|
41
|
-
assert.ok(options.args.includes('--no-first-run'));
|
|
60
|
+
assert.equal(options.args.includes('--start-maximized'), false);
|
|
42
61
|
});
|
|
43
62
|
|
|
44
63
|
it('forwards ignored defaults to both browser engines', () => {
|
|
45
64
|
const playwright = buildPlaywrightLaunchOptions({
|
|
46
65
|
headless: false,
|
|
47
|
-
slowMo:
|
|
66
|
+
slowMo: 0,
|
|
48
67
|
chromeArgs: [],
|
|
49
68
|
ignoreDefaultArgs: ['--no-first-run'],
|
|
50
69
|
});
|
|
@@ -99,7 +118,7 @@ describe('browser launch options', () => {
|
|
|
99
118
|
it('omits browser selection options by default', () => {
|
|
100
119
|
const playwright = buildPlaywrightLaunchOptions({
|
|
101
120
|
headless: false,
|
|
102
|
-
slowMo:
|
|
121
|
+
slowMo: 0,
|
|
103
122
|
chromeArgs: [],
|
|
104
123
|
});
|
|
105
124
|
const puppeteer = buildPuppeteerLaunchOptions({
|