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.
Files changed (155) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +216 -32
  3. package/bin/browser-commander.js +16 -0
  4. package/eslint.config.js +10 -0
  5. package/examples/react-test-app/src/App.jsx +24 -23
  6. package/extension/background.js +14 -0
  7. package/extension/manifest.json +12 -0
  8. package/extension/relay-handler.js +353 -0
  9. package/package.json +25 -6
  10. package/scripts/run-tests.mjs +4 -2
  11. package/src/browser/attach/differences.js +131 -0
  12. package/src/browser/attach/extension-relay.js +402 -0
  13. package/src/browser/attach/index.js +81 -0
  14. package/src/browser/attach/snapshot-launch.js +146 -0
  15. package/src/browser/attach/snapshot.js +374 -0
  16. package/src/browser/attach/websocket.js +615 -0
  17. package/src/browser/browser-cookie-cache.js +12 -17
  18. package/src/browser/browser-cookie-credentials.js +6 -9
  19. package/src/browser/browser-cookies.js +5 -2
  20. package/src/browser/cdp-session.js +94 -0
  21. package/src/browser/connector.js +42 -4
  22. package/src/browser/debugging-port.js +155 -0
  23. package/src/browser/launch-options.js +63 -52
  24. package/src/browser/launch-restrictions.json +147 -0
  25. package/src/browser/launcher.js +329 -133
  26. package/src/browser/media.js +5 -0
  27. package/src/browser/migration/bookmarks.js +73 -0
  28. package/src/browser/migration/chromium-crypto.js +89 -0
  29. package/src/browser/migration/cookies.js +155 -0
  30. package/src/browser/migration/extensions.js +187 -0
  31. package/src/browser/migration/firefox-bookmarks.js +144 -0
  32. package/src/browser/migration/firefox-der.js +84 -0
  33. package/src/browser/migration/firefox-nss.js +281 -0
  34. package/src/browser/migration/firefox.js +334 -0
  35. package/src/browser/migration/fs-utils.js +51 -0
  36. package/src/browser/migration/history.js +103 -0
  37. package/src/browser/migration/index.js +307 -0
  38. package/src/browser/migration/os-crypt-keys.js +106 -0
  39. package/src/browser/migration/passwords.js +171 -0
  40. package/src/browser/migration/preferences.js +132 -0
  41. package/src/browser/migration/sqlite-snapshot.js +138 -0
  42. package/src/browser/open-in-user-browser.js +112 -0
  43. package/src/browser/parity.js +439 -0
  44. package/src/browser/profile-directory.js +102 -0
  45. package/src/browser/real-browser.js +513 -306
  46. package/src/browser/restrictions.js +148 -0
  47. package/src/browser/system-browser.js +232 -0
  48. package/src/browser/webdriver.js +572 -0
  49. package/src/cli/args.js +253 -0
  50. package/src/cli/commands.js +332 -0
  51. package/src/cli/dispatcher.js +99 -0
  52. package/src/cli/handle-methods.js +136 -0
  53. package/src/cli/handles.js +273 -0
  54. package/src/cli/high-level-methods.js +310 -0
  55. package/src/cli/main.js +67 -0
  56. package/src/cli/modules.js +100 -0
  57. package/src/cli/rpc-error.js +77 -0
  58. package/src/cli/script.js +77 -0
  59. package/src/cli/serve.js +144 -0
  60. package/src/cli/sessions.js +195 -0
  61. package/src/core/constants.js +7 -1
  62. package/src/core/engine-adapter.js +107 -1
  63. package/src/core/engine-detection.js +18 -3
  64. package/src/core/navigation-safety.js +4 -0
  65. package/src/core/webdriver-conversions.js +347 -0
  66. package/src/core/webdriver-events.js +229 -0
  67. package/src/core/webdriver-page.js +716 -0
  68. package/src/downloads/attach.js +6 -0
  69. package/src/downloads/manager.js +9 -0
  70. package/src/downloads/sources.js +71 -0
  71. package/src/downloads/staging.js +21 -9
  72. package/src/elements/selectors.js +3 -2
  73. package/src/elements/visibility.js +4 -0
  74. package/src/exports.js +61 -2
  75. package/src/factory.js +19 -4
  76. package/src/fingerprint/apply.js +6 -0
  77. package/src/fingerprint/automation-parity.js +7 -8
  78. package/src/fingerprint/limitations.js +1 -1
  79. package/src/fingerprint/limitations.json +45 -2
  80. package/src/parity/harness.js +303 -0
  81. package/src/parity/probe.js +838 -0
  82. package/src/parity/version-page.js +144 -0
  83. package/src/traces/bundle.js +10 -2
  84. package/src/traces/recorder.js +16 -3
  85. package/src/utilities/subprocess.js +199 -0
  86. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  87. package/tests/e2e/cli.e2e.test.js +160 -0
  88. package/tests/e2e/downloads.e2e.test.js +79 -2
  89. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  90. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  91. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  92. package/tests/e2e/playwright.e2e.test.js +25 -17
  93. package/tests/e2e/puppeteer.e2e.test.js +17 -23
  94. package/tests/e2e/webdriver.e2e.test.js +239 -0
  95. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  96. package/tests/helpers/cli-fakes.js +214 -0
  97. package/tests/helpers/declared-api.js +80 -0
  98. package/tests/helpers/e2e-browser.js +40 -14
  99. package/tests/helpers/fake-chrome.js +124 -0
  100. package/tests/helpers/migration-fixtures.js +242 -0
  101. package/tests/helpers/temp-directory.js +24 -0
  102. package/tests/helpers/trace-fixtures.js +18 -0
  103. package/tests/helpers/webdriver-mocks.js +183 -0
  104. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  105. package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
  106. package/tests/unit/browser/attach/snapshot.test.js +390 -0
  107. package/tests/unit/browser/attach/websocket.test.js +240 -0
  108. package/tests/unit/browser/browser-cookies.test.js +2 -32
  109. package/tests/unit/browser/cdp-session.test.js +162 -0
  110. package/tests/unit/browser/connector.test.js +23 -0
  111. package/tests/unit/browser/launch-building-blocks.test.js +238 -0
  112. package/tests/unit/browser/launch-options.test.js +41 -22
  113. package/tests/unit/browser/launcher.test.js +281 -102
  114. package/tests/unit/browser/media.test.js +13 -1
  115. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  116. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  117. package/tests/unit/browser/migration/cookies.test.js +94 -0
  118. package/tests/unit/browser/migration/extensions.test.js +104 -0
  119. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  120. package/tests/unit/browser/migration/firefox.test.js +139 -0
  121. package/tests/unit/browser/migration/history.test.js +48 -0
  122. package/tests/unit/browser/migration/index.test.js +117 -0
  123. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  124. package/tests/unit/browser/migration/passwords.test.js +144 -0
  125. package/tests/unit/browser/migration/preferences.test.js +77 -0
  126. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  127. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  128. package/tests/unit/browser/parity.test.js +281 -0
  129. package/tests/unit/browser/real-browser.test.js +458 -33
  130. package/tests/unit/browser/webdriver.test.js +473 -0
  131. package/tests/unit/cli/bin.test.js +69 -0
  132. package/tests/unit/cli/declared-api.test.js +49 -0
  133. package/tests/unit/cli/dispatcher.test.js +429 -0
  134. package/tests/unit/cli/handles.test.js +170 -0
  135. package/tests/unit/cli/main.test.js +409 -0
  136. package/tests/unit/cli/serve.test.js +167 -0
  137. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  138. package/tests/unit/core/webdriver-page.test.js +534 -0
  139. package/tests/unit/downloads/manager.test.js +82 -9
  140. package/tests/unit/downloads/staging.test.js +24 -2
  141. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  142. package/tests/unit/fingerprint/limitations.test.js +1 -1
  143. package/tests/unit/parity/harness.test.js +138 -0
  144. package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
  145. package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
  146. package/tests/unit/scripts/feature-parity.test.js +221 -0
  147. package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
  148. package/tests/unit/scripts/pre-commit-config.test.js +2 -1
  149. package/tests/unit/traces/bundle.test.js +24 -3
  150. package/tests/unit/traces/links.test.js +7 -0
  151. package/tests/unit/traces/reader.test.js +11 -4
  152. package/tests/unit/traces/recorder.test.js +4 -0
  153. package/tests/unit/traces/viewer.test.js +4 -0
  154. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  155. 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('applies safe defaults, appends extra arguments, and supports per-flag opt-out', () => {
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.ok(options.args.includes('--password-store=basic'));
19
- assert.ok(options.args.includes('--no-first-run'));
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('can ignore every Browser Commander default', () => {
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, ['--lang=en-US']);
32
- assert.equal(options.ignoreDefaultArgs, true);
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('can opt out of the password-store default specifically', () => {
36
- const options = resolveChromeArgs({
37
- ignoreDefaultArgs: ['--password-store=basic'],
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('--password-store=basic'), false);
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: 150,
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: 150,
121
+ slowMo: 0,
103
122
  chromeArgs: [],
104
123
  });
105
124
  const puppeteer = buildPuppeteerLaunchOptions({