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.
- package/CHANGELOG.md +145 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +19 -4
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- 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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
108
|
-
|
|
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
|
|
113
|
-
[
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
131
|
-
|
|
138
|
+
with a fixed `--remote-debugging-port` and a dedicated `--user-data-dir`, or
|
|
139
|
+
let `launchBrowser()`/`launchRealBrowser()` start it.
|
|
132
140
|
|
|
133
|
-
`launchRealBrowser()`
|
|
134
|
-
Edge, Brave, or Chromium
|
|
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
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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:
|
|
378
|
-
|
|
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'
|
|
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`.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
returns the spawned `browserProcess`,
|
|
421
|
-
|
|
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
|
-
`
|
|
424
|
-
|
|
425
|
-
|
|
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
|
+
}
|