browser-commander 0.20.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 (139) hide show
  1. package/CHANGELOG.md +145 -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 +19 -4
  10. package/src/browser/attach/differences.js +131 -0
  11. package/src/browser/attach/extension-relay.js +402 -0
  12. package/src/browser/attach/index.js +81 -0
  13. package/src/browser/attach/snapshot-launch.js +146 -0
  14. package/src/browser/attach/snapshot.js +374 -0
  15. package/src/browser/attach/websocket.js +615 -0
  16. package/src/browser/browser-cookie-cache.js +12 -17
  17. package/src/browser/browser-cookie-credentials.js +6 -9
  18. package/src/browser/browser-cookies.js +5 -2
  19. package/src/browser/cdp-session.js +94 -0
  20. package/src/browser/connector.js +42 -4
  21. package/src/browser/debugging-port.js +155 -0
  22. package/src/browser/launch-options.js +63 -52
  23. package/src/browser/launch-restrictions.json +147 -0
  24. package/src/browser/launcher.js +329 -133
  25. package/src/browser/media.js +5 -0
  26. package/src/browser/migration/bookmarks.js +73 -0
  27. package/src/browser/migration/chromium-crypto.js +89 -0
  28. package/src/browser/migration/cookies.js +155 -0
  29. package/src/browser/migration/extensions.js +187 -0
  30. package/src/browser/migration/firefox-bookmarks.js +144 -0
  31. package/src/browser/migration/firefox-der.js +84 -0
  32. package/src/browser/migration/firefox-nss.js +281 -0
  33. package/src/browser/migration/firefox.js +334 -0
  34. package/src/browser/migration/fs-utils.js +51 -0
  35. package/src/browser/migration/history.js +103 -0
  36. package/src/browser/migration/index.js +307 -0
  37. package/src/browser/migration/os-crypt-keys.js +106 -0
  38. package/src/browser/migration/passwords.js +171 -0
  39. package/src/browser/migration/preferences.js +132 -0
  40. package/src/browser/migration/sqlite-snapshot.js +138 -0
  41. package/src/browser/open-in-user-browser.js +112 -0
  42. package/src/browser/parity.js +439 -0
  43. package/src/browser/profile-directory.js +102 -0
  44. package/src/browser/real-browser.js +513 -306
  45. package/src/browser/restrictions.js +148 -0
  46. package/src/browser/system-browser.js +232 -0
  47. package/src/browser/webdriver.js +572 -0
  48. package/src/cli/args.js +253 -0
  49. package/src/cli/commands.js +332 -0
  50. package/src/cli/dispatcher.js +99 -0
  51. package/src/cli/handle-methods.js +136 -0
  52. package/src/cli/handles.js +273 -0
  53. package/src/cli/high-level-methods.js +310 -0
  54. package/src/cli/main.js +67 -0
  55. package/src/cli/modules.js +100 -0
  56. package/src/cli/rpc-error.js +77 -0
  57. package/src/cli/script.js +77 -0
  58. package/src/cli/serve.js +144 -0
  59. package/src/cli/sessions.js +195 -0
  60. package/src/core/constants.js +7 -1
  61. package/src/core/engine-adapter.js +107 -1
  62. package/src/core/engine-detection.js +18 -3
  63. package/src/core/navigation-safety.js +4 -0
  64. package/src/core/webdriver-conversions.js +347 -0
  65. package/src/core/webdriver-events.js +229 -0
  66. package/src/core/webdriver-page.js +716 -0
  67. package/src/downloads/attach.js +6 -0
  68. package/src/elements/selectors.js +3 -2
  69. package/src/elements/visibility.js +4 -0
  70. package/src/exports.js +61 -2
  71. package/src/factory.js +19 -4
  72. package/src/fingerprint/apply.js +6 -0
  73. package/src/fingerprint/automation-parity.js +7 -8
  74. package/src/fingerprint/limitations.js +1 -1
  75. package/src/fingerprint/limitations.json +45 -2
  76. package/src/parity/harness.js +303 -0
  77. package/src/parity/probe.js +838 -0
  78. package/src/parity/version-page.js +144 -0
  79. package/src/utilities/subprocess.js +199 -0
  80. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  81. package/tests/e2e/cli.e2e.test.js +160 -0
  82. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  83. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  84. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  85. package/tests/e2e/playwright.e2e.test.js +25 -17
  86. package/tests/e2e/puppeteer.e2e.test.js +17 -23
  87. package/tests/e2e/webdriver.e2e.test.js +239 -0
  88. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  89. package/tests/helpers/cli-fakes.js +214 -0
  90. package/tests/helpers/declared-api.js +80 -0
  91. package/tests/helpers/e2e-browser.js +40 -14
  92. package/tests/helpers/fake-chrome.js +124 -0
  93. package/tests/helpers/migration-fixtures.js +242 -0
  94. package/tests/helpers/temp-directory.js +24 -0
  95. package/tests/helpers/webdriver-mocks.js +183 -0
  96. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  97. package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
  98. package/tests/unit/browser/attach/snapshot.test.js +390 -0
  99. package/tests/unit/browser/attach/websocket.test.js +240 -0
  100. package/tests/unit/browser/browser-cookies.test.js +2 -32
  101. package/tests/unit/browser/cdp-session.test.js +162 -0
  102. package/tests/unit/browser/connector.test.js +23 -0
  103. package/tests/unit/browser/launch-building-blocks.test.js +238 -0
  104. package/tests/unit/browser/launch-options.test.js +41 -22
  105. package/tests/unit/browser/launcher.test.js +281 -102
  106. package/tests/unit/browser/media.test.js +13 -1
  107. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  108. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  109. package/tests/unit/browser/migration/cookies.test.js +94 -0
  110. package/tests/unit/browser/migration/extensions.test.js +104 -0
  111. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  112. package/tests/unit/browser/migration/firefox.test.js +139 -0
  113. package/tests/unit/browser/migration/history.test.js +48 -0
  114. package/tests/unit/browser/migration/index.test.js +117 -0
  115. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  116. package/tests/unit/browser/migration/passwords.test.js +144 -0
  117. package/tests/unit/browser/migration/preferences.test.js +77 -0
  118. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  119. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  120. package/tests/unit/browser/parity.test.js +281 -0
  121. package/tests/unit/browser/real-browser.test.js +458 -33
  122. package/tests/unit/browser/webdriver.test.js +473 -0
  123. package/tests/unit/cli/bin.test.js +69 -0
  124. package/tests/unit/cli/declared-api.test.js +49 -0
  125. package/tests/unit/cli/dispatcher.test.js +429 -0
  126. package/tests/unit/cli/handles.test.js +170 -0
  127. package/tests/unit/cli/main.test.js +409 -0
  128. package/tests/unit/cli/serve.test.js +167 -0
  129. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  130. package/tests/unit/core/webdriver-page.test.js +534 -0
  131. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  132. package/tests/unit/fingerprint/limitations.test.js +1 -1
  133. package/tests/unit/parity/harness.test.js +138 -0
  134. package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
  135. package/tests/unit/scripts/feature-parity.test.js +221 -0
  136. package/tests/unit/traces/reader.test.js +1 -0
  137. package/tests/unit/traces/recorder.test.js +1 -0
  138. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  139. package/tests/unit/utilities/subprocess.test.js +80 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,150 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1d398f4: Run a real browser by default, measure parity, migrate profiles and drive
8
+ every engine from one CLI (#101, #102, #103, #104, #105).
9
+
10
+ Launch the installed browser the way a person would, and make every restriction
11
+ opt-in (#101, #103).
12
+
13
+ `launchBrowser()` now starts the installed Chrome (or `channel`/
14
+ `executablePath`) itself and attaches over CDP. The whole command line is
15
+ `--user-data-dir=<fresh temporary profile> --remote-debugging-port=<reserved
16
+ port> about:blank` (a URL in `args` replaces `about:blank`): no `--enable-automation`, no `--disable-blink-features`, so
17
+ `navigator.webdriver` is `false` and Chrome shows neither the "controlled by
18
+ automated test software" nor the unsupported-flag infobar. The temporary
19
+ profile is seeded with the `First Run` sentinel and `Local State` entries that
20
+ keep the "What's new" tab and Microsoft Edge's first-run tab from stealing the
21
+ foreground, and it is deleted on close. `launch: 'engine'` keeps the Playwright/Puppeteer launcher.
22
+
23
+ The debugging port is reserved on loopback and its ownership is proven from the
24
+ browser's own `DevTools listening on` line, so a port lost to another process is
25
+ detected and the launch retried on a new port. Subprocesses are started through
26
+ `command-stream`.
27
+
28
+ Everything the library used to add on its own - `CHROME_ARGS`, the Google API
29
+ key override, the Translate preference, `--start-maximized` and `slowMo: 150` -
30
+ is gone from the defaults and available as named `restrictions` from the shared
31
+ `launch-restrictions.json` (`restrictions: ['legacy-defaults']` restores the old
32
+ switches). The host `process.env` is never modified; restriction environment
33
+ reaches the browser process only. `LAUNCH_MODES`, `LAUNCH_RESTRICTIONS`,
34
+ `LAUNCH_RESTRICTION_PRESETS`, `resolveRestrictions`, `mergeFeatureSwitches`,
35
+ `resolveLaunchExecutable`, the profile-directory helpers,
36
+ `reserveLoopbackPort` and `PortRaceError` are exported.
37
+
38
+ Measure how far a driven browser is from the same browser started by hand
39
+ (#103).
40
+
41
+ `measureParity()` starts the binary the way a person would, reads its
42
+ chrome://version command line and runs the environment probe in it, then does
43
+ the same for the browser Browser Commander launches (or for a `session` you
44
+ pass). Every difference in the command line, in the feature switches and in
45
+ anything a page can read is tagged with the limitations-catalogue entry that
46
+ explains it or marked `requested` when it follows from an option you set; a
47
+ report with an unexplained difference is not `ok`. `parseSwitches`,
48
+ `compareCommandLines`, `classifyDifferences` and `readBrowserVersionPage` are
49
+ exported too. The parity e2e suite measures every installed Chrome-family
50
+ browser (Chrome, Chromium, Edge, Brave), headful and headless, through both
51
+ engines.
52
+
53
+ `--headless` is no longer treated as an AutomationControlled trigger, so a
54
+ headless launch no longer adds `--disable-blink-features=AutomationControlled`:
55
+ Chrome 153 keeps `navigator.webdriver` false for every `--headless` form, and
56
+ the extra switch was the one difference `measureParity()` found headless.
57
+
58
+ Add opt-in profile migration and a no-automation "open in the user's browser"
59
+ mode (#102).
60
+
61
+ The real-browser launch stays clean by default - so signing in with a Google
62
+ account works (a nonexistent address shows "Couldn't find your Google Account",
63
+ not "This browser or app may not be secure"). New APIs close the gap to the
64
+ user's real browser:
65
+
66
+ - `migrateProfile({ from, to, include, domains, targetBrowser })` copies data
67
+ from an installed browser's main profile into a dedicated target profile. It
68
+ is strictly read-only on the source: SQLite databases (`History`, `Top Sites`,
69
+ cookies, Firefox `places.sqlite`) are copied through the SQLite Online Backup
70
+ API so a running browser is never disturbed, and JSON (`Bookmarks`,
71
+ `Preferences`) is copied as is. Data classes are `cookies`, `bookmarks`,
72
+ `history`, `passwords`, `preferences` and `extensions` (`ALL_DATA_CLASSES`).
73
+ Chromium passwords are re-encrypted with the target profile's OS-keystore key;
74
+ Firefox uses its NSS `key4.db` path. The report explains what was skipped and
75
+ why: DBSC-bound Google cookies (`dbsc-bound`), Windows app-bound `v20`
76
+ (`app-bound-v20`), migrated extensions whose `Secure Preferences` MAC cannot be
77
+ forged (`mac-will-not-validate`), and Firefox primary passwords
78
+ (`primary-password-set`).
79
+ - `launchRealBrowser({ migrateFrom })` runs that migration before launch, seeds
80
+ the migrated cookies automatically, and returns a `migration` report on the
81
+ session.
82
+ - `openInUserBrowser(url)` opens a URL in the user's own default browser with no
83
+ automation (macOS `open`, Linux `xdg-open`, Windows `explorer.exe`) for flows that
84
+ only need to show a page, such as an OAuth or CLI web-login screen.
85
+
86
+ `migrateProfile`, `ALL_DATA_CLASSES`, `openInUserBrowser`, `buildOpenCommand`
87
+ and `validateOpenUrl` are exported.
88
+
89
+ Add the `browser-commander` command line and its `serve --stdio` JSON-RPC
90
+ bridge (#104).
91
+
92
+ The package now installs a `browser-commander` binary. Its commands are
93
+ `version`, `launch [--keep-open]`, `open`, `goto`, `click`, `fill`, `eval`,
94
+ `screenshot`, `pdf`, `trace start|stop|view`, `cookies import`,
95
+ `profile migrate`, `doctor`, `run <script.json>` and `serve --stdio`. Each
96
+ command prints exactly one JSON document to stdout. It exits `0` on success,
97
+ `1` on error, `2` when `doctor` finds an unlisted difference, and `64` on a
98
+ usage error. Page commands either attach to a browser with `--cdp-endpoint` or
99
+ launch a temporary one. The Rust and Python CLIs follow the same contract,
100
+ `docs/cli-and-bridge.md`.
101
+
102
+ `run` and `serve --stdio` share one dispatcher. It has the high-level methods
103
+ (`session.launch`, `page.goto`, …) and generic handle methods
104
+ (`handle.root`, `handle.call`, `handle.get`, `handle.describe`,
105
+ `handle.dispose`, `events.subscribe`/`unsubscribe`). The handle methods reach
106
+ every public Playwright and Puppeteer method, and an e2e suite checks this
107
+ against the engines' shipped `.d.ts` files. Values that are not JSON are
108
+ tagged (`$handle`, `$binary`, `$function`, `$date`, …), and handle ids are
109
+ deterministic. The shared CLI contract checks equivalent results through all
110
+ three language entry points. The Rust crate's existing 27-operation
111
+ `node_engine_bridge.js` remains available for its typed adapter; callers can
112
+ use the generic CLI bridge to reach methods beyond that adapter.
113
+
114
+ `createCdpSession(page)` and `commander.createCdpSession()` return one raw CDP
115
+ surface (`send`, `on`, `once`, `off`, `detach`) for Playwright and Puppeteer
116
+ pages. The cookie credential tools now run through `command-stream`, like the
117
+ browser launch.
118
+
119
+ Add a `selenium` engine that drives Chrome and Firefox over W3C WebDriver and
120
+ WebDriver BiDi (#104).
121
+
122
+ `selenium-webdriver` is an optional peer dependency. `makeBrowserCommander()`
123
+ detects a selenium `WebDriver` and runs every commander operation through a
124
+ Puppeteer-shaped `WebDriverPage` facade and a `SeleniumAdapter` that uses
125
+ WebDriver's own Element Click, Element Clear and Element Send Keys. PDFs come
126
+ from the W3C Print Page command; options it cannot honour (header/footer
127
+ templates, `preferCSSPageSize`) are refused rather than dropped.
128
+
129
+ `launchWebDriver({ browser, executablePath, driverPath, headless, userDataDir,
130
+ bidi })` finds a driver (`driverPath`, then `PATH`, then the Selenium Manager
131
+ bundled with `selenium-webdriver`), starts it on a reserved loopback port, waits
132
+ for `/status`, and opens a session with a temporary profile that `close()`
133
+ removes. Chrome gets the real-browser command line and all 19 switches
134
+ chromedriver would add on its own are excluded, so `navigator.webdriver` is
135
+ `false` headful and headless (measured with Chrome and chromedriver 153).
136
+ `connectWebDriver({ serverUrl, capabilities })` opens a session on a server that
137
+ is already running.
138
+
139
+ With `bidi: true` the session asks for `webSocketUrl`, and console, dialog,
140
+ request and frame-navigation events, preload scripts and full-page screenshots
141
+ work through WebDriver BiDi. The adapter adds `onConsoleMessage()`,
142
+ `onNavigation()`, `onBidiEvent()` and `navigate()` helpers. Media emulation,
143
+ fingerprint overrides and managed downloads need CDP and throw a clear error on
144
+ this engine. Four new shared limitations (`webdriver-driver-launch-switches`,
145
+ `webdriver-no-cdp-emulation`, `webdriver-classic-has-no-events`,
146
+ `webdriver-print-options`) record the gaps for JavaScript, Python and Rust.
147
+
3
148
  ## 0.20.0
4
149
 
5
150
  ### Minor Changes
package/README.md CHANGED
@@ -8,7 +8,7 @@ A universal browser automation library for JavaScript/TypeScript that supports b
8
8
  npm install browser-commander
9
9
  ```
10
10
 
11
- You'll also need either Playwright or Puppeteer:
11
+ You'll also need Playwright, Puppeteer or Selenium WebDriver:
12
12
 
13
13
  ```bash
14
14
  # With Playwright
@@ -16,6 +16,9 @@ npm install playwright
16
16
 
17
17
  # Or with Puppeteer
18
18
  npm install puppeteer
19
+
20
+ # Or with Selenium WebDriver (W3C WebDriver + WebDriver BiDi)
21
+ npm install selenium-webdriver
19
22
  ```
20
23
 
21
24
  ## Documentation
@@ -93,26 +96,31 @@ await commander.destroy();
93
96
  await browser.close();
94
97
  ```
95
98
 
96
- To reuse an installed Chrome-family browser instead of downloading the
97
- engine's bundled Chromium, select a browser channel or provide its executable
98
- path. Both options are passed directly to Playwright or Puppeteer:
99
+ `launchBrowser()` starts the installed Chrome the way a person would and then
100
+ attaches to it. The whole command line is
101
+ `--user-data-dir=<fresh temporary profile> --remote-debugging-port=<reserved port> about:blank`:
102
+ no automation switches, so `navigator.webdriver` is `false`, there is no
103
+ "controlled by automated test software" or unsupported-flag infobar, and
104
+ extensions, sync, translation and every other browser feature behave as in a
105
+ hand-started Chrome. The temporary profile is deleted by `browser.close()`.
99
106
 
100
107
  ```javascript
101
108
  const { browser, page } = await launchBrowser({
102
109
  engine: 'playwright',
103
- channel: 'chrome',
104
- // Or: executablePath: '/usr/bin/google-chrome',
105
- headless: true,
110
+ channel: 'msedge', // or executablePath: '/usr/bin/google-chrome'
111
+ userDataDir: '/tmp/my-profile', // keep the profile instead of a temporary one
106
112
  extraArgs: ['--lang=en-US'],
107
- // Per-flag escape hatch; `args` remains a compatible append-only alias.
108
- ignoreDefaultArgs: ['--disable-infobars'],
113
+ // Opt-in restrictions, by name (see the table linked below).
114
+ restrictions: ['no-sync', 'no-translate'],
109
115
  });
110
116
  ```
111
117
 
112
- Browser Commander applies the documented
113
- [automation-friendly launch defaults](../docs/feature-parity.md#automation-friendly-launch-defaults),
114
- including `--password-store=basic` to avoid an additional OS credential
115
- dialog. `ignoreDefaultArgs: true` omits every Browser Commander default.
118
+ Every switch Browser Commander used to add on its own is available as a named
119
+ [launch restriction](../docs/feature-parity.md#launch-command-line-and-opt-in-restrictions);
120
+ `restrictions: ['legacy-defaults']` restores the old `CHROME_ARGS`. Pass
121
+ `launch: 'engine'` to use Playwright's `launchPersistentContext()` or
122
+ `puppeteer.launch()` instead; the engine then adds its own switches, and
123
+ `ignoreDefaultArgs` removes them.
116
124
 
117
125
  Attach to a Chrome-family browser that is already listening for CDP connections:
118
126
 
@@ -127,11 +135,13 @@ const commander = makeBrowserCommander({ page });
127
135
  ```
128
136
 
129
137
  Attachment cannot change the existing process's launch arguments. Start it
130
- with a loopback remote-debugging port, a dedicated `--user-data-dir`, and the
131
- automation-friendly defaults above, or use `launchRealBrowser()`.
138
+ with a fixed `--remote-debugging-port` and a dedicated `--user-data-dir`, or
139
+ let `launchBrowser()`/`launchRealBrowser()` start it.
132
140
 
133
- `launchRealBrowser()` can find and start a genuine installed Chrome,
134
- Edge, Brave, or Chromium with a loopback CDP endpoint and then attach to it. It
141
+ `launchRealBrowser()` is the lower-level helper behind the default launch: it
142
+ finds and starts an installed Chrome, Edge, Brave, or Chromium on a reserved
143
+ loopback port, confirms from the browser's own `DevTools listening on` line
144
+ that the port is really its own (retrying on a port race), and attaches. It
135
145
  always uses a dedicated profile; Chrome 136 and newer do not honor remote
136
146
  debugging switches for the default profile. See the
137
147
  [Chrome remote-debugging security change](https://developer.chrome.com/blog/remote-debugging-port).
@@ -144,7 +154,7 @@ const connection = await launchRealBrowser({
144
154
  channel: 'chrome',
145
155
  userDataDir: '/tmp/my-automation-profile',
146
156
  extraArgs: ['--lang=en-US'],
147
- ignoreDefaultArgs: ['--disable-infobars'],
157
+ restrictions: ['no-default-browser-check'],
148
158
  seedCookies: [
149
159
  { name: 'session', value: 'saved', url: 'https://example.com' },
150
160
  ],
@@ -188,8 +198,8 @@ scripts touch Keychain, libsecret/KWallet, or DPAPI at most once per TTL window.
188
198
  Set `refresh: true` to force a new read, customize `cache.dir`/`ttlMinutes`, or
189
199
  set `cache: false` to opt out of disk caching.
190
200
  `launchAndConnectRealBrowser()` remains available as a descriptive alias.
191
- Remote-debugging, loopback, and profile arguments remain managed even when all
192
- optional defaults are ignored; `headless: true` adds `--headless=new`.
201
+ The remote-debugging and profile switches are always managed;
202
+ `headless: true` adds `--headless=new`.
193
203
 
194
204
  Reuse a saved authenticated session by passing Playwright-compatible storage
195
205
  state as a JSON file path or object. Cookies and localStorage are restored for
@@ -371,17 +381,27 @@ commander.pageTrigger({
371
381
  ### launchBrowser(options)
372
382
 
373
383
  ```javascript
374
- const { browser, page } = await launchBrowser({
384
+ const { browser, page, close } = await launchBrowser({
375
385
  engine: 'playwright', // 'playwright' or 'puppeteer'
386
+ launch: 'real', // 'real' (start the installed browser, attach) or 'engine'
387
+ channel: 'chrome', // or executablePath; defaults to the installed Chrome
376
388
  headless: false, // Run in headless mode
377
- userDataDir: '~/.hh-apply/playwright-data', // Browser profile directory
378
- slowMo: 150, // Slow down operations (ms)
389
+ userDataDir: undefined, // Keep a profile; default is a fresh temporary one
390
+ restrictions: [], // Opt-in restrictions such as 'no-extensions', 'no-sync'
391
+ slowMo: 0, // Slow down operations (ms)
379
392
  verbose: false, // Enable debug logging
380
- args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append
393
+ args: ['--no-sandbox'], // Custom Chrome args to append
381
394
  storageState: './session-state.json', // Saved cookies and localStorage, as a path or object
382
395
  });
383
396
  ```
384
397
 
398
+ With Playwright, `browser` is the attached browser's default context, so
399
+ `browser.pages()`, `browser.newPage()` and `browser.close()` work as they did
400
+ with the persistent context. `close()` (and `browser.close()`) closes the
401
+ browser and deletes a temporary profile. The result also carries `launch`,
402
+ `userDataDir`, `temporaryProfile` and the exact `args` the browser was started
403
+ with.
404
+
385
405
  The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
386
406
 
387
407
  The `storageState` option accepts a Playwright-compatible JSON path or object.
@@ -414,15 +434,84 @@ Playwright accepts `timeout` and Puppeteer accepts `protocolTimeout`.
414
434
  Start an installed browser and connect through `connectBrowser()`. Use
415
435
  `channel` (`chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`,
416
436
  `msedge-beta`, `msedge-dev`, `msedge-canary`, `brave`, or `chromium`) or an
417
- explicit `executablePath`. The helper defaults to
418
- a managed directory under `~/.browser-commander/real-browser/`, rejects known
419
- default browser-profile paths, protects its remote-debugging arguments, and
420
- returns the spawned `browserProcess`, resolved `cdpEndpoint`, executable path,
421
- and profile path alongside `{ browser, page }`.
437
+ explicit `executablePath`. Without `userDataDir` it creates a fresh temporary
438
+ profile that `close()` deletes; it rejects known default browser-profile paths,
439
+ protects its remote-debugging switches, reserves a loopback port (or uses
440
+ `remoteDebuggingPort`, never `0`), and returns the spawned `browserProcess`,
441
+ `cdpEndpoint`, `remoteDebuggingPort`, `executablePath`, `userDataDir`,
442
+ `temporaryProfile` and `args` alongside `{ browser, page, close }`.
422
443
  `launchAndConnectRealBrowser()` is an alias with identical behavior.
423
- `extraArgs` appends custom switches, while `ignoreDefaultArgs` accepts a list
424
- of defaults to omit (or `true` to omit all optional defaults). The older
425
- `args` append option remains supported.
444
+ `restrictions` opts into named restrictions, `args`/`extraArgs` append custom
445
+ switches, and `env` adds environment variables for the browser process only.
446
+
447
+ The launch is clean by default, so signing in with a Google account works
448
+ (a nonexistent address shows "Couldn't find your Google Account" rather than
449
+ "This browser or app may not be secure"). Pass `migrateFrom` to opt into a
450
+ read-only migration from an installed browser's main profile before the launch;
451
+ the migrated cookies are seeded automatically and the returned session carries a
452
+ `migration` report (see `migrateProfile()` below):
453
+
454
+ ```javascript
455
+ const session = await launchRealBrowser({
456
+ channel: 'chrome',
457
+ userDataDir, // dedicated; clean by default
458
+ migrateFrom: {
459
+ browser: 'chrome', // chrome | edge | brave | chromium | firefox
460
+ profile: 'Default',
461
+ include: ['cookies', 'bookmarks', 'history', 'passwords', 'preferences'],
462
+ domains: ['npmjs.com', 'github.com'], // optional cookie filter
463
+ },
464
+ });
465
+ console.log(session.migration.migrated, session.migration.skipped);
466
+ ```
467
+
468
+ ### launchWebDriver(options) / connectWebDriver(options)
469
+
470
+ The `selenium` engine drives Chrome or Firefox over W3C WebDriver through
471
+ `selenium-webdriver` (an optional peer dependency). `launchWebDriver()` finds a
472
+ driver (`driverPath`, then `PATH`, then the Selenium Manager bundled with
473
+ `selenium-webdriver`, which downloads a chromedriver/geckodriver matching the
474
+ browser), starts it on a reserved loopback port, waits for `/status`, and opens
475
+ a session with a fresh temporary profile that `close()` deletes:
476
+
477
+ ```javascript
478
+ import { launchWebDriver, makeBrowserCommander } from 'browser-commander';
479
+
480
+ const { driver, page, close } = await launchWebDriver({
481
+ browser: 'chrome', // or 'firefox'
482
+ headless: false,
483
+ bidi: true, // WebDriver BiDi: console/dialog/network events, preload scripts
484
+ // executablePath, driverPath, userDataDir, args, restrictions, env
485
+ });
486
+ const commander = makeBrowserCommander({ page }); // commander.engine === 'selenium'
487
+
488
+ page.on('console', (message) => console.log(message.text())); // needs bidi
489
+ await commander.goto({ url: 'https://example.com' });
490
+ await commander.fill({ selector: 'input[name="q"]', text: 'hello' });
491
+ await commander.click({ selector: 'button[type="submit"]' });
492
+ const pdf = await page.pdf({ format: 'A4' }); // W3C Print Page
493
+ await close();
494
+ ```
495
+
496
+ `page` is a `WebDriverPage`: a Puppeteer-shaped facade over the `driver`
497
+ (`evaluate`, `$`, `$$`, `keyboard`, `mouse`, `screenshot`, `pdf`, `cookies`,
498
+ `on`), so the rest of Browser Commander runs unchanged. `makeBrowserCommander()`
499
+ also accepts a bare selenium `WebDriver` and wraps it. On the adapter
500
+ (`createEngineAdapter(page, 'selenium')`), `onConsoleMessage(handler)`,
501
+ `onNavigation(handler)`, `onBidiEvent(method, handler)` and
502
+ `navigate(url, {wait})` expose BiDi `log.entryAdded` and `browsingContext`
503
+ directly; they throw when the session has no BiDi.
504
+
505
+ Chrome gets the same command line as `launchRealBrowser()`
506
+ (`--user-data-dir` and a fixed `--remote-debugging-port`), and every switch
507
+ chromedriver would add on its own - `--enable-automation`,
508
+ `--remote-debugging-port=0`, `--password-store=basic`, `--test-type=webdriver`
509
+ and 16 more - is excluded, so `navigator.webdriver` is `false` headful and
510
+ headless. `connectWebDriver({ serverUrl, capabilities })` opens a session on a
511
+ server that is already running (a Selenium Grid, a cloud provider); pass
512
+ `capabilities: { webSocketUrl: true }` for BiDi. Media emulation, fingerprint
513
+ overrides and managed downloads need CDP and throw on this engine; see
514
+ [docs/feature-parity.md](../docs/feature-parity.md#webdriver-engine).
426
515
 
427
516
  ### listBrowserProfiles(options)
428
517
 
@@ -466,6 +555,75 @@ cookies is acceptable.
466
555
  Treat imported cookies like passwords: keep cache directories private, use a
467
556
  short TTL, never commit them, and seed only a dedicated automation profile.
468
557
 
558
+ ### migrateProfile(options)
559
+
560
+ Copy data from an installed browser's main profile into a dedicated target
561
+ profile directory (usually the `Default` folder of a `launchRealBrowser()`
562
+ `userDataDir`). This is the same migration `launchRealBrowser({ migrateFrom })`
563
+ runs, exposed directly. It is **opt-in and strictly read-only** on the source:
564
+ SQLite databases (`History`, `Top Sites`, cookies, Firefox `places.sqlite`) are
565
+ copied through the SQLite Online Backup API so a running browser is never
566
+ disturbed, and JSON files (`Bookmarks`, `Preferences`) are copied as is.
567
+
568
+ ```javascript
569
+ import { migrateProfile } from 'browser-commander';
570
+
571
+ const report = await migrateProfile({
572
+ from: { browser: 'chrome', profile: 'Default' },
573
+ to: '/path/to/dedicated/User Data/Default',
574
+ targetBrowser: 'chrome', // launching channel, for key derivation
575
+ include: [
576
+ 'cookies',
577
+ 'bookmarks',
578
+ 'history',
579
+ 'passwords',
580
+ 'preferences',
581
+ 'extensions',
582
+ ],
583
+ domains: ['github.com'], // optional cookie filter
584
+ });
585
+ ```
586
+
587
+ The report is `{ source, target, migrated, skipped, warnings, cookies }`:
588
+ `migrated` counts each data class, `cookies` holds the seedable Playwright-shape
589
+ cookies, and `skipped`/`warnings` explain everything that could not be migrated
590
+ verbatim, for example:
591
+
592
+ - **DBSC-bound Google cookies** (`__Secure-1PSIDTS`/`3PSIDTS`/`SIDTS`) are
593
+ reported `dbsc-bound`: their Device Bound Session Credentials key cannot leave
594
+ the source profile, so those sessions expire quickly and are not copied.
595
+ - **Windows app-bound `v20`** passwords/cookies are reported `app-bound-v20`
596
+ because they require the browser's privileged elevation service.
597
+ - **Migrated extensions** are reported `mac-will-not-validate`: Chromium's
598
+ `Secure Preferences` MAC cannot be forged externally, so migrated extensions
599
+ are likely disabled until re-enabled in the browser.
600
+ - **Firefox with a primary password** is reported `primary-password-set`.
601
+
602
+ Data classes: `cookies`, `bookmarks`, `history`, `passwords`, `preferences`,
603
+ `extensions` (exported as `ALL_DATA_CLASSES`). Chromium sources
604
+ (`chrome`/`edge`/`brave`/`chromium`) re-encrypt passwords with the target
605
+ profile's OS-keystore key; Firefox uses its own NSS (`key4.db`) path for
606
+ `cookies.sqlite`, `places.sqlite` and `logins.json`.
607
+
608
+ ### openInUserBrowser(url)
609
+
610
+ Open a URL in the user's own default browser with **no automation** - no CDP,
611
+ no dedicated profile, no `navigator.webdriver`. Use it when a consumer only has
612
+ to show a page where the user is already signed in (an OAuth consent screen or a
613
+ CLI web-login page). It shells out to the platform opener (macOS `open`, Linux
614
+ `xdg-open`, Windows `explorer.exe`) after validating the URL:
615
+
616
+ ```javascript
617
+ import { openInUserBrowser } from 'browser-commander';
618
+
619
+ await openInUserBrowser('https://github.com/login/device');
620
+ ```
621
+
622
+ Only a browser-safe scheme set is accepted (`http:`, `https:`, `file:`, `ftp:`,
623
+ `about:`, `chrome:`, `edge:`, `view-source:`); `validateOpenUrl()` and
624
+ `buildOpenCommand()` are exported for callers that want to validate or build the
625
+ command separately.
626
+
469
627
  ### saveStorageState(page, filePath)
470
628
 
471
629
  Save the current cookies and localStorage in Playwright's portable storage
@@ -852,6 +1010,32 @@ await commander.page.pdf({ format: 'A4' });
852
1010
 
853
1011
  This is the **official extensibility mechanism** while awaiting browser-commander to add first-class support for these APIs. Please [report missing APIs](https://github.com/link-foundation/browser-commander/issues) so they can be added.
854
1012
 
1013
+ ## Command Line
1014
+
1015
+ The package installs a `browser-commander` command. The Rust and Python packages ship the same command, and the contract all three follow is [docs/cli-and-bridge.md](../docs/cli-and-bridge.md). Every command prints exactly one JSON document to stdout. The exit code is `0` on success and `1` on error. `doctor` exits `2` when it finds an unlisted difference, and a usage error exits `64`.
1016
+
1017
+ ```bash
1018
+ npx browser-commander version
1019
+ npx browser-commander goto https://example.com --headless
1020
+
1021
+ # Keep one browser running and drive it from later commands
1022
+ npx browser-commander launch --keep-open &
1023
+ npx browser-commander fill '#q' 'hello' --cdp-endpoint http://127.0.0.1:9222
1024
+ npx browser-commander eval 'document.title' --cdp-endpoint http://127.0.0.1:9222
1025
+
1026
+ # Run a JSON command script (see tests/cli-contract/basic.json)
1027
+ npx browser-commander run script.json --engine puppeteer
1028
+
1029
+ # JSON-RPC 2.0 over stdin/stdout, one message per line
1030
+ echo '{"jsonrpc":"2.0","id":1,"method":"version"}' | npx browser-commander serve --stdio
1031
+ ```
1032
+
1033
+ The other commands are `open`, `click`, `screenshot`, `pdf`, `trace start|stop|view`, `cookies import`, `profile migrate` and `doctor`. Use the `=` form for values that start with `--`, for example `--arg=--lang=de`.
1034
+
1035
+ `serve --stdio` exposes the high-level methods (`session.launch`, `page.goto`, …). It also exposes generic handle methods (`handle.root`, `handle.call`, `handle.get`, `handle.describe`, `events.subscribe`, …), which reach every public Playwright and Puppeteer method. The Rust and Python ports use this bridge for the engines they do not implement natively.
1036
+
1037
+ For raw Chrome DevTools Protocol access from JavaScript, `createCdpSession(page)` (or `commander.createCdpSession()`) returns the same `send`/`on`/`once`/`off`/`detach` surface for Playwright and Puppeteer pages.
1038
+
855
1039
  ## Debugging
856
1040
 
857
1041
  Enable verbose mode for detailed logs:
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `browser-commander` command (issue #104). See docs/cli-and-bridge.md.
4
+ *
5
+ * Stdout carries exactly one JSON document (or JSON-RPC lines for
6
+ * `serve --stdio`), so library logging is moved to stderr.
7
+ */
8
+ import { runCli } from '../src/cli/main.js';
9
+
10
+ for (const method of ['log', 'info', 'debug']) {
11
+ console[method] = (...args) => console.error(...args);
12
+ }
13
+
14
+ const exitCode = await runCli(process.argv.slice(2));
15
+ // Engines can keep sockets or timers alive; exit once stdout is flushed.
16
+ process.stdout.write('', () => process.exit(exitCode));
package/eslint.config.js CHANGED
@@ -131,6 +131,16 @@ export default [
131
131
  'no-unused-vars': 'off',
132
132
  },
133
133
  },
134
+ {
135
+ // The environment probe is source text too: `measureParity()` wraps it as
136
+ // `(<file contents>)()` for `page.evaluate` and a plain `<script>` tag, and
137
+ // its length is the surface of the browser API it measures.
138
+ files: ['src/parity/probe.js'],
139
+ rules: {
140
+ 'no-unused-vars': 'off',
141
+ 'max-lines-per-function': 'off',
142
+ },
143
+ },
134
144
  {
135
145
  // Test files have different requirements
136
146
  files: ['tests/**/*.js', '**/*.test.js'],
@@ -1,18 +1,20 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ const createEmptyFormData = () => ({
4
+ name: '',
5
+ email: '',
6
+ password: '',
7
+ bio: '',
8
+ gender: '',
9
+ interests: [],
10
+ country: '',
11
+ newsletter: false,
12
+ terms: false,
13
+ });
2
14
 
3
15
  function App() {
4
16
  // Form state
5
- const [formData, setFormData] = useState({
6
- name: '',
7
- email: '',
8
- password: '',
9
- bio: '',
10
- gender: '',
11
- interests: [],
12
- country: '',
13
- newsletter: false,
14
- terms: false,
15
- });
17
+ const [formData, setFormData] = useState(createEmptyFormData);
16
18
 
17
19
  const [submitResult, setSubmitResult] = useState(null);
18
20
  const [counter, setCounter] = useState(0);
@@ -23,6 +25,15 @@ function App() {
23
25
  const [dynamicContent, setDynamicContent] = useState(null);
24
26
  const [isLoading, setIsLoading] = useState(false);
25
27
 
28
+ useEffect(() => {
29
+ if (!showModal) return;
30
+ const closeOnEscape = (event) => {
31
+ if (event.key === 'Escape') setShowModal(false);
32
+ };
33
+ document.addEventListener('keydown', closeOnEscape);
34
+ return () => document.removeEventListener('keydown', closeOnEscape);
35
+ }, [showModal]);
36
+
26
37
  const handleInputChange = (e) => {
27
38
  const { name, value, type, checked } = e.target;
28
39
 
@@ -56,17 +67,7 @@ function App() {
56
67
  };
57
68
 
58
69
  const handleReset = () => {
59
- setFormData({
60
- name: '',
61
- email: '',
62
- password: '',
63
- bio: '',
64
- gender: '',
65
- interests: [],
66
- country: '',
67
- newsletter: false,
68
- terms: false,
69
- });
70
+ setFormData(createEmptyFormData());
70
71
  setSubmitResult(null);
71
72
  };
72
73
 
@@ -0,0 +1,14 @@
1
+ /* global chrome, WebSocket */
2
+ import { createRelayClient } from './relay-handler.js';
3
+
4
+ /**
5
+ * Service worker of the Browser Commander Relay extension (issue #102,
6
+ * addendum, mode 2). All logic lives in relay-handler.js; this file only
7
+ * wires it to the real `chrome` namespace. `start()` runs synchronously at top
8
+ * level because MV3 only delivers events to listeners registered then.
9
+ */
10
+ createRelayClient({
11
+ chrome,
12
+ WebSocket,
13
+ userAgent: navigator.userAgent,
14
+ }).start();
@@ -0,0 +1,12 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Browser Commander Relay",
4
+ "version": "0.20.0",
5
+ "description": "Lets Browser Commander on this computer drive tabs of this browser through chrome.debugger, over a loopback-only connection.",
6
+ "minimum_chrome_version": "116",
7
+ "permissions": ["debugger", "tabs", "alarms", "storage"],
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ }
12
+ }