browser-commander 0.20.0 → 0.21.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.
Files changed (169) hide show
  1. package/.jscpd.json +2 -1
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +159 -0
  4. package/README.md +216 -32
  5. package/bin/browser-commander.js +16 -0
  6. package/eslint.config.js +10 -0
  7. package/examples/react-test-app/src/App.jsx +24 -23
  8. package/extension/background.js +14 -0
  9. package/extension/manifest.json +12 -0
  10. package/extension/relay-handler.js +353 -0
  11. package/package.json +21 -4
  12. package/src/browser/attach/differences.js +131 -0
  13. package/src/browser/attach/extension-relay.js +402 -0
  14. package/src/browser/attach/index.js +81 -0
  15. package/src/browser/attach/snapshot-launch.js +146 -0
  16. package/src/browser/attach/snapshot.js +374 -0
  17. package/src/browser/attach/websocket.js +615 -0
  18. package/src/browser/browser-cookie-cache.js +26 -19
  19. package/src/browser/browser-cookie-credentials.js +6 -9
  20. package/src/browser/browser-cookies.js +5 -2
  21. package/src/browser/cdp-session.js +94 -0
  22. package/src/browser/connector.js +42 -4
  23. package/src/browser/debugging-port.js +155 -0
  24. package/src/browser/launch-options.js +63 -52
  25. package/src/browser/launch-restrictions.json +147 -0
  26. package/src/browser/launcher.js +329 -133
  27. package/src/browser/media.js +5 -0
  28. package/src/browser/migration/bookmarks.js +73 -0
  29. package/src/browser/migration/chromium-crypto.js +89 -0
  30. package/src/browser/migration/cookies.js +155 -0
  31. package/src/browser/migration/extensions.js +187 -0
  32. package/src/browser/migration/firefox-bookmarks.js +144 -0
  33. package/src/browser/migration/firefox-der.js +84 -0
  34. package/src/browser/migration/firefox-nss.js +281 -0
  35. package/src/browser/migration/firefox.js +334 -0
  36. package/src/browser/migration/fs-utils.js +51 -0
  37. package/src/browser/migration/history.js +103 -0
  38. package/src/browser/migration/index.js +307 -0
  39. package/src/browser/migration/os-crypt-keys.js +106 -0
  40. package/src/browser/migration/passwords.js +171 -0
  41. package/src/browser/migration/preferences.js +132 -0
  42. package/src/browser/migration/sqlite-snapshot.js +138 -0
  43. package/src/browser/open-in-user-browser.js +112 -0
  44. package/src/browser/parity.js +439 -0
  45. package/src/browser/profile-directory.js +205 -0
  46. package/src/browser/real-browser.js +545 -306
  47. package/src/browser/restrictions.js +148 -0
  48. package/src/browser/system-browser.js +232 -0
  49. package/src/browser/webdriver.js +572 -0
  50. package/src/cli/args.js +300 -0
  51. package/src/cli/commands.js +332 -0
  52. package/src/cli/dispatcher.js +99 -0
  53. package/src/cli/handle-methods.js +136 -0
  54. package/src/cli/handles.js +273 -0
  55. package/src/cli/high-level-methods.js +310 -0
  56. package/src/cli/main.js +67 -0
  57. package/src/cli/modules.js +100 -0
  58. package/src/cli/rpc-error.js +77 -0
  59. package/src/cli/script.js +77 -0
  60. package/src/cli/serve.js +144 -0
  61. package/src/cli/sessions.js +199 -0
  62. package/src/core/constants.js +7 -1
  63. package/src/core/engine-adapter.js +107 -1
  64. package/src/core/engine-detection.js +18 -3
  65. package/src/core/navigation-safety.js +4 -0
  66. package/src/core/webdriver-conversions.js +347 -0
  67. package/src/core/webdriver-events.js +229 -0
  68. package/src/core/webdriver-page.js +716 -0
  69. package/src/downloads/attach.js +6 -0
  70. package/src/elements/selectors.js +3 -2
  71. package/src/elements/visibility.js +4 -0
  72. package/src/exports.js +61 -2
  73. package/src/factory.js +19 -4
  74. package/src/fingerprint/apply.js +6 -0
  75. package/src/fingerprint/automation-parity.js +7 -8
  76. package/src/fingerprint/limitations.js +1 -1
  77. package/src/fingerprint/limitations.json +45 -2
  78. package/src/parity/harness.js +303 -0
  79. package/src/parity/probe.js +838 -0
  80. package/src/parity/version-page.js +144 -0
  81. package/src/traces/bundle.js +3 -2
  82. package/src/traces/observers.js +14 -5
  83. package/src/traces/recorder.js +3 -0
  84. package/src/traces/viewer.js +16 -8
  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/fingerprint-parity.e2e.test.js +104 -46
  89. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  90. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  91. package/tests/e2e/playwright.e2e.test.js +26 -17
  92. package/tests/e2e/puppeteer.e2e.test.js +18 -23
  93. package/tests/e2e/webdriver.e2e.test.js +239 -0
  94. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  95. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
  96. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
  97. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
  98. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
  99. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
  100. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
  101. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
  102. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
  103. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
  104. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
  105. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
  106. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
  107. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
  108. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
  109. package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
  110. package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
  111. package/tests/fixtures/traces/conformance/scenario.json +249 -0
  112. package/tests/helpers/cli-fakes.js +214 -0
  113. package/tests/helpers/declared-api.js +80 -0
  114. package/tests/helpers/e2e-browser.js +40 -14
  115. package/tests/helpers/fake-chrome.js +124 -0
  116. package/tests/helpers/migration-fixtures.js +242 -0
  117. package/tests/helpers/temp-directory.js +24 -0
  118. package/tests/helpers/webdriver-mocks.js +183 -0
  119. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  120. package/tests/unit/browser/attach/snapshot-launch.test.js +321 -0
  121. package/tests/unit/browser/attach/snapshot.test.js +409 -0
  122. package/tests/unit/browser/attach/websocket.test.js +240 -0
  123. package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
  124. package/tests/unit/browser/browser-cookies.test.js +2 -32
  125. package/tests/unit/browser/cdp-session.test.js +162 -0
  126. package/tests/unit/browser/connector.test.js +23 -0
  127. package/tests/unit/browser/launch-building-blocks.test.js +361 -0
  128. package/tests/unit/browser/launch-options.test.js +41 -22
  129. package/tests/unit/browser/launcher.test.js +281 -102
  130. package/tests/unit/browser/media.test.js +13 -1
  131. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  132. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  133. package/tests/unit/browser/migration/cookies.test.js +94 -0
  134. package/tests/unit/browser/migration/extensions.test.js +104 -0
  135. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  136. package/tests/unit/browser/migration/firefox.test.js +139 -0
  137. package/tests/unit/browser/migration/history.test.js +48 -0
  138. package/tests/unit/browser/migration/index.test.js +117 -0
  139. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  140. package/tests/unit/browser/migration/passwords.test.js +144 -0
  141. package/tests/unit/browser/migration/preferences.test.js +77 -0
  142. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  143. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  144. package/tests/unit/browser/parity.test.js +281 -0
  145. package/tests/unit/browser/real-browser.test.js +458 -33
  146. package/tests/unit/browser/storage-state.test.js +2 -0
  147. package/tests/unit/browser/webdriver.test.js +473 -0
  148. package/tests/unit/cli/bin.test.js +69 -0
  149. package/tests/unit/cli/declared-api.test.js +49 -0
  150. package/tests/unit/cli/dispatcher.test.js +429 -0
  151. package/tests/unit/cli/handles.test.js +170 -0
  152. package/tests/unit/cli/main.test.js +437 -0
  153. package/tests/unit/cli/serve.test.js +167 -0
  154. package/tests/unit/core/navigation-manager.test.js +11 -1
  155. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  156. package/tests/unit/core/webdriver-page.test.js +534 -0
  157. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  158. package/tests/unit/fingerprint/limitations.test.js +1 -1
  159. package/tests/unit/parity/harness.test.js +138 -0
  160. package/tests/unit/playwright-protocol-coverage.test.js +231 -0
  161. package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
  162. package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
  163. package/tests/unit/scripts/feature-parity.test.js +327 -0
  164. package/tests/unit/traces/conformance.test.js +92 -0
  165. package/tests/unit/traces/reader.test.js +1 -0
  166. package/tests/unit/traces/recorder.test.js +1 -0
  167. package/tests/unit/traces/viewer.test.js +78 -0
  168. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  169. package/tests/unit/utilities/subprocess.test.js +80 -0
@@ -0,0 +1,361 @@
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
+ mkdir,
10
+ mkdtemp,
11
+ readFile,
12
+ rm,
13
+ stat,
14
+ writeFile,
15
+ } from 'node:fs/promises';
16
+ import {
17
+ LAUNCH_RESTRICTIONS,
18
+ LAUNCH_RESTRICTION_PRESETS,
19
+ mergeFeatureSwitches,
20
+ resolveRestrictions,
21
+ } from '../../../src/browser/restrictions.js';
22
+ import {
23
+ FIRST_RUN_SENTINEL,
24
+ INITIAL_LOCAL_STATE,
25
+ LOCAL_STATE_FILE,
26
+ PREFERENCES_FILE,
27
+ TEMPORARY_PROFILE_PREFIX,
28
+ createTemporaryUserDataDir,
29
+ configureUserDataDir,
30
+ prepareUserDataDir,
31
+ removeUserDataDir,
32
+ } from '../../../src/browser/profile-directory.js';
33
+ import {
34
+ PortRaceError,
35
+ assertFixedDebuggingPort,
36
+ classifyDevToolsOwnership,
37
+ parseDevToolsOutput,
38
+ reserveLoopbackPort,
39
+ watchDevToolsOutput,
40
+ } from '../../../src/browser/debugging-port.js';
41
+ import { CHROME_ARGS } from '../../../src/core/constants.js';
42
+ import { detectAutomationControlledTriggers } from '../../../src/fingerprint/automation-parity.js';
43
+
44
+ describe('launch restrictions', () => {
45
+ it('has unique ids and a description for each entry', () => {
46
+ const ids = LAUNCH_RESTRICTIONS.map((entry) => entry.id);
47
+ assert.equal(new Set(ids).size, ids.length);
48
+ for (const entry of LAUNCH_RESTRICTIONS) {
49
+ assert.ok(entry.description.length > 10, entry.id);
50
+ assert.ok(entry.args || entry.disableFeatures || entry.env, entry.id);
51
+ }
52
+ });
53
+
54
+ it('never offers a switch that turns AutomationControlled on', () => {
55
+ for (const entry of LAUNCH_RESTRICTIONS) {
56
+ assert.deepEqual(
57
+ detectAutomationControlledTriggers(entry.args ?? []),
58
+ [],
59
+ entry.id
60
+ );
61
+ }
62
+ });
63
+
64
+ it('keeps the pre-#103 defaults available as a preset', () => {
65
+ const { args } = resolveRestrictions(['legacy-defaults']);
66
+ assert.deepEqual([...args].sort(), [...CHROME_ARGS].sort());
67
+ });
68
+
69
+ it('adds the Google key override only to the browser environment', () => {
70
+ const resolved = resolveRestrictions(['legacy-launch-browser']);
71
+ assert.deepEqual(resolved.env, {
72
+ GOOGLE_API_KEY: 'no',
73
+ GOOGLE_DEFAULT_CLIENT_ID: 'no',
74
+ GOOGLE_DEFAULT_CLIENT_SECRET: 'no',
75
+ });
76
+ assert.ok(resolved.args.includes('--disable-features=Translate'));
77
+ assert.ok(LAUNCH_RESTRICTION_PRESETS['legacy-launch-browser']);
78
+ });
79
+
80
+ it('deduplicates ids and joins feature lists into one switch', () => {
81
+ const resolved = resolveRestrictions([
82
+ 'no-translate',
83
+ 'no-lens',
84
+ 'no-translate',
85
+ ]);
86
+ assert.deepEqual(resolved.ids, ['no-translate', 'no-lens']);
87
+ assert.deepEqual(resolved.args, [
88
+ '--disable-features=Translate,LensOverlay',
89
+ ]);
90
+ });
91
+
92
+ it('rejects unknown names and non-arrays', () => {
93
+ assert.throws(() => resolveRestrictions(['nope']), RangeError);
94
+ assert.throws(() => resolveRestrictions('no-sync'), TypeError);
95
+ });
96
+
97
+ it('merges repeated feature switches into the first position', () => {
98
+ assert.deepEqual(
99
+ mergeFeatureSwitches([
100
+ '--a',
101
+ '--disable-features=A,B',
102
+ '--enable-features=X',
103
+ '--disable-features=B,C',
104
+ '--b',
105
+ ]),
106
+ ['--a', '--disable-features=A,B,C', '--enable-features=X', '--b']
107
+ );
108
+ });
109
+ });
110
+
111
+ describe('profile directory', () => {
112
+ const readPreferences = async (directory) =>
113
+ JSON.parse(
114
+ await readFile(path.join(directory, 'Default', 'Preferences'), 'utf8')
115
+ );
116
+
117
+ it('creates a fresh profile with the First Run sentinel', async () => {
118
+ const dir = await createTemporaryUserDataDir();
119
+ try {
120
+ assert.ok(path.basename(dir).startsWith(TEMPORARY_PROFILE_PREFIX));
121
+ await access(path.join(dir, FIRST_RUN_SENTINEL));
122
+ assert.deepEqual(
123
+ JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8')),
124
+ {
125
+ browser: {
126
+ ...INITIAL_LOCAL_STATE.browser,
127
+ default_browser_infobar_declined_count: 5,
128
+ default_browser_declined_count: 5,
129
+ },
130
+ fre: INITIAL_LOCAL_STATE.fre,
131
+ }
132
+ );
133
+ assert.deepEqual(await readPreferences(dir), {
134
+ browser: { check_default_browser: false },
135
+ });
136
+ } finally {
137
+ await removeUserDataDir(dir);
138
+ }
139
+ await assert.rejects(() => access(dir), /ENOENT/);
140
+ });
141
+
142
+ it('preserves other Local State values that Chrome already wrote', async () => {
143
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
144
+ try {
145
+ const localState = path.join(dir, LOCAL_STATE_FILE);
146
+ await writeFile(
147
+ localState,
148
+ '{"browser":{"enabled_labs_experiments":[]}}'
149
+ );
150
+ await prepareUserDataDir(dir);
151
+ assert.deepEqual(JSON.parse(await readFile(localState, 'utf8')), {
152
+ browser: {
153
+ enabled_labs_experiments: [],
154
+ default_browser_infobar_declined_count: 5,
155
+ default_browser_declined_count: 5,
156
+ },
157
+ });
158
+ } finally {
159
+ await rm(dir, { recursive: true, force: true });
160
+ }
161
+ });
162
+
163
+ it('deep-merges settings and lets the named default-browser option override a copied preference', async () => {
164
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
165
+ try {
166
+ await prepareUserDataDir(dir);
167
+ await writeFile(
168
+ path.join(dir, 'Default', 'Preferences'),
169
+ JSON.stringify({
170
+ browser: { check_default_browser: true, show_home_button: false },
171
+ intl: { accept_languages: 'en' },
172
+ })
173
+ );
174
+ await prepareUserDataDir(dir, {
175
+ defaultBrowserCheck: false,
176
+ preferences: { browser: { show_home_button: true } },
177
+ localState: { browser: { extra: 1 } },
178
+ });
179
+ assert.deepEqual(await readPreferences(dir), {
180
+ browser: { check_default_browser: false, show_home_button: true },
181
+ intl: { accept_languages: 'en' },
182
+ });
183
+ assert.deepEqual(
184
+ JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8')),
185
+ {
186
+ browser: {
187
+ last_whats_new_version: 9999,
188
+ default_browser_infobar_declined_count: 5,
189
+ default_browser_declined_count: 5,
190
+ extra: 1,
191
+ },
192
+ fre: { has_user_seen_fre: true },
193
+ }
194
+ );
195
+ await prepareUserDataDir(dir, { defaultBrowserCheck: true });
196
+ assert.equal(
197
+ JSON.parse(
198
+ await readFile(path.join(dir, 'Default', 'Preferences'), 'utf8')
199
+ ).browser.check_default_browser,
200
+ true
201
+ );
202
+ assert.equal(
203
+ JSON.parse(await readFile(path.join(dir, LOCAL_STATE_FILE), 'utf8'))
204
+ .browser.default_browser_declined_count,
205
+ 0
206
+ );
207
+ } finally {
208
+ await rm(dir, { recursive: true, force: true });
209
+ }
210
+ });
211
+
212
+ it('keeps an existing sentinel untouched', async () => {
213
+ const parent = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
214
+ try {
215
+ const dir = path.join(parent, 'nested', 'profile');
216
+ await prepareUserDataDir(dir);
217
+ const sentinel = path.join(dir, FIRST_RUN_SENTINEL);
218
+ const before = await stat(sentinel);
219
+ await prepareUserDataDir(dir);
220
+ assert.equal((await stat(sentinel)).mtimeMs, before.mtimeMs);
221
+ assert.equal(await readFile(sentinel, 'utf8'), '');
222
+ } finally {
223
+ await rm(parent, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ it('allows an explicit first-run flow in a new profile', async () => {
228
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
229
+ try {
230
+ await prepareUserDataDir(dir, { firstRun: true });
231
+ await assert.rejects(
232
+ () => access(path.join(dir, FIRST_RUN_SENTINEL)),
233
+ /ENOENT/
234
+ );
235
+ } finally {
236
+ await rm(dir, { recursive: true, force: true });
237
+ }
238
+ });
239
+
240
+ it('rejects malformed profile settings before writing them', async () => {
241
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
242
+ try {
243
+ await assert.rejects(
244
+ prepareUserDataDir(dir, { preferences: { browser: false } }),
245
+ /preferences.browser/u
246
+ );
247
+ await assert.rejects(
248
+ prepareUserDataDir(dir, { preferences: JSON.parse('{"__proto__":1}') }),
249
+ /profile setting key/u
250
+ );
251
+ } finally {
252
+ await rm(dir, { recursive: true, force: true });
253
+ }
254
+ });
255
+
256
+ it('configures a selected snapshot profile instead of Default', async () => {
257
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'bc-profile-test-'));
258
+ try {
259
+ const selected = path.join(dir, 'Profile 1');
260
+ await mkdir(selected);
261
+ await writeFile(
262
+ path.join(selected, 'Preferences'),
263
+ '{"browser":{"check_default_browser":true}}'
264
+ );
265
+ await configureUserDataDir(dir, { profileDirectory: 'Profile 1' });
266
+ assert.equal(
267
+ JSON.parse(await readFile(path.join(selected, 'Preferences'), 'utf8'))
268
+ .browser.check_default_browser,
269
+ false
270
+ );
271
+ await assert.rejects(
272
+ () => access(path.join(dir, PREFERENCES_FILE)),
273
+ /ENOENT/
274
+ );
275
+ } finally {
276
+ await rm(dir, { recursive: true, force: true });
277
+ }
278
+ });
279
+ });
280
+
281
+ describe('debugging port', () => {
282
+ it('reserves a loopback port that can be bound again', async () => {
283
+ const port = await reserveLoopbackPort();
284
+ assert.ok(port > 0 && port < 65_536);
285
+ await new Promise((resolve, reject) => {
286
+ const server = net.createServer();
287
+ server.once('error', reject);
288
+ server.listen({ port, host: '127.0.0.1' }, () => server.close(resolve));
289
+ });
290
+ });
291
+
292
+ it('refuses port 0 and out-of-range ports', () => {
293
+ assert.throws(() => assertFixedDebuggingPort(0), /AutomationControlled/);
294
+ assert.throws(() => assertFixedDebuggingPort(70_000), RangeError);
295
+ assert.throws(() => assertFixedDebuggingPort('9222'), RangeError);
296
+ assert.equal(assertFixedDebuggingPort(9222), 9222);
297
+ });
298
+
299
+ it('parses the DevTools listening line', () => {
300
+ assert.deepEqual(
301
+ parseDevToolsOutput(
302
+ '\nDevTools listening on ws://127.0.0.1:9222/devtools/browser/1-2\n'
303
+ ),
304
+ {
305
+ listening: {
306
+ url: 'ws://127.0.0.1:9222/devtools/browser/1-2',
307
+ host: '127.0.0.1',
308
+ port: 9222,
309
+ },
310
+ bindFailed: false,
311
+ }
312
+ );
313
+ assert.equal(
314
+ parseDevToolsOutput(
315
+ 'DevTools listening on ws://[::1]:9222/devtools/browser/x'
316
+ ).listening.host,
317
+ '::1'
318
+ );
319
+ });
320
+
321
+ it('classifies ownership, fallback and give-up output', () => {
322
+ const owned = parseDevToolsOutput(
323
+ 'DevTools listening on ws://127.0.0.1:9222/devtools/browser/x'
324
+ );
325
+ assert.equal(classifyDevToolsOwnership(owned, 9222), 'owned');
326
+ assert.equal(classifyDevToolsOwnership(owned, 9223), 'race');
327
+ const fallback = parseDevToolsOutput(
328
+ 'bind() failed\nDevTools listening on ws://[::1]:9222/devtools/browser/x'
329
+ );
330
+ assert.equal(classifyDevToolsOwnership(fallback, 9222), 'race');
331
+ assert.equal(
332
+ classifyDevToolsOwnership(
333
+ parseDevToolsOutput('Cannot start http server for devtools.'),
334
+ 9222
335
+ ),
336
+ 'race'
337
+ );
338
+ // Unrelated bind failures (mDNS, media router) are not conclusive.
339
+ assert.equal(
340
+ classifyDevToolsOwnership(parseDevToolsOutput('bind() failed'), 9222),
341
+ 'pending'
342
+ );
343
+ });
344
+
345
+ it('watches a stream across chunk boundaries', () => {
346
+ const stream = new PassThrough();
347
+ const watcher = watchDevToolsOutput(stream);
348
+ assert.equal(watcher.available, true);
349
+ stream.emit('data', 'DevTools listening on ws://127.0.0.1:92');
350
+ assert.equal(watcher.state().listening, null);
351
+ stream.emit('data', '22/devtools/browser/abc\n');
352
+ assert.equal(watcher.state().listening.port, 9222);
353
+ assert.equal(watchDevToolsOutput(undefined).available, false);
354
+ });
355
+
356
+ it('names the port in race errors', () => {
357
+ const error = new PortRaceError(9222, 'bind failed');
358
+ assert.equal(error.port, 9222);
359
+ assert.match(error.message, /9222.*bind failed/);
360
+ });
361
+ });
@@ -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({