Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ A universal browser automation library with a unified API across multiple browse
See [docs/feature-parity.md](docs/feature-parity.md) for the cross-language feature matrix and [docs/case-studies/issue-51/README.md](docs/case-studies/issue-51/README.md) for the implementation notes.

All three implementations can attach to a running Chrome-family browser over
CDP. The JavaScript package also provides `launchAndConnectRealBrowser()` to
find and start an installed Chrome, Edge, Brave, or Chromium with a safe,
dedicated automation profile before attaching.
CDP or find and start an installed Chrome, Edge, Brave, or Chromium with a
safe, dedicated automation profile before attaching. Use
`launchRealBrowser()` in JavaScript and `launch_real_browser()` in Python or
Rust.

## Core Concept: Page State Machine

Expand Down
11 changes: 11 additions & 0 deletions docs/feature-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ This matrix tracks the shared API surface across the maintained language impleme
| Rust API docs | `cd rust && cargo doc --no-deps --all-features` | Built by `.github/workflows/docs.yml` |
| Combined Pages artifact | Generated from both outputs | Uploaded on PRs and deployed from `main` |

## Real-Browser Lifecycle Parity

| Capability | JavaScript | Rust | Python |
| --------------------------------------- | --------------------- | ------------------------------------ | ----------------------- |
| Attach to an existing CDP endpoint | Playwright, Puppeteer | Chromiumoxide, Playwright, Puppeteer | Playwright, Selenium |
| Discover an installed Chrome-family app | Linux, macOS, Windows | Linux, macOS, Windows | Linux, macOS, Windows |
| Launch with a dedicated profile | `launchRealBrowser()` | `launch_real_browser()` | `launch_real_browser()` |
| Loopback-only CDP readiness probe | Supported | Supported | Supported |
| Seed cookies after connection | Supported | Supported | Supported |
| Return browser and page handles | Raw engine handles | Shared `EngineAdapter` | Raw engine handles |

## Compatibility Notes

- Existing Rust aliases remain compatible: `chromiumoxide` and `cdp` parse as `EngineType::Chromiumoxide`; `fantoccini` and `webdriver` parse as `EngineType::Fantoccini`.
Expand Down
4 changes: 2 additions & 2 deletions experiments/connect-real-browser-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import os from "node:os";
import path from "node:path";

import {
launchAndConnectRealBrowser,
launchRealBrowser,
makeBrowserCommander,
} from "../js/src/index.js";

Expand Down Expand Up @@ -37,7 +37,7 @@ async function waitForExit(browserProcess) {

try {
for (const engine of ["playwright", "puppeteer"]) {
const connection = await launchAndConnectRealBrowser({
const connection = await launchRealBrowser({
engine,
executablePath: browserExecutable,
userDataDir: path.join(temporaryDirectory, engine),
Expand Down
63 changes: 63 additions & 0 deletions experiments/launch-real-browser-python-smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Launch system Chrome through the Python real-browser lifecycle helper."""

from __future__ import annotations

import asyncio
import shutil
import sys
import tempfile
from pathlib import Path

REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPOSITORY_ROOT / "python" / "src"))

from browser_commander import ( # noqa: E402
RealBrowserOptions,
launch_real_browser,
make_browser_commander,
)


async def main() -> None:
executable = sys.argv[1] if len(sys.argv) > 1 else "/usr/bin/google-chrome"
profile = Path(tempfile.mkdtemp(prefix="browser-commander-python-real-"))
result = await launch_real_browser(
RealBrowserOptions(
engine="playwright",
executable_path=executable,
user_data_dir=str(profile),
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage"],
seed_cookies=[
{
"name": "attached",
"value": "python",
"url": "https://example.com",
}
],
)
)

try:
await result.page.goto(
"data:text/html,<main id=connected>Real browser connection works</main>"
)
commander = make_browser_commander(
result.page,
enable_network_tracking=False,
enable_navigation_manager=False,
enable_dialog_manager=False,
)
assert await commander.count("#connected") == 1
await commander.destroy()
print("python real-browser launch-and-connect smoke test passed")
finally:
await result.browser.close()
if result.browser_process.returncode is None:
result.browser_process.terminate()
await result.browser_process.wait()
shutil.rmtree(profile, ignore_errors=True)


if __name__ == "__main__":
asyncio.run(main())
5 changes: 5 additions & 0 deletions js/.changeset/real-browser-launch-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'browser-commander': minor
---

Add the `launchRealBrowser()` API name for launching and attaching to a genuine installed Chrome-family browser.
10 changes: 6 additions & 4 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,16 @@ const { browser, page } = await connectBrowser({
const commander = makeBrowserCommander({ page });
```

`launchAndConnectRealBrowser()` can find and start a genuine installed Chrome,
`launchRealBrowser()` can find and start a genuine installed Chrome,
Edge, Brave, or Chromium with a loopback CDP endpoint and then attach to it. It
always uses a dedicated profile; Chrome 136 and newer do not honor remote
debugging switches for the default profile. See the
[Chrome remote-debugging security change](https://developer.chrome.com/blog/remote-debugging-port).

```javascript
import { launchAndConnectRealBrowser } from 'browser-commander';
import { launchRealBrowser } from 'browser-commander';

const connection = await launchAndConnectRealBrowser({
const connection = await launchRealBrowser({
engine: 'puppeteer',
channel: 'chrome',
userDataDir: '/tmp/my-automation-profile',
Expand All @@ -140,6 +140,7 @@ await connection.browser.close();

Cookie seeding copies only cookies you explicitly provide; the helper does not
read, decrypt, or expose cookies from a browser's default profile.
`launchAndConnectRealBrowser()` remains available as a descriptive alias.

Reuse a saved authenticated session by passing Playwright-compatible storage
state as a JSON file path or object. Cookies and localStorage are restored for
Expand Down Expand Up @@ -359,7 +360,7 @@ const { browser, page } = await connectBrowser({
Playwright accepts `timeout` and Puppeteer accepts `protocolTimeout`.
`storageState` can also seed Playwright-compatible cookies and localStorage.

### launchAndConnectRealBrowser(options)
### launchRealBrowser(options)

Start an installed browser and connect through `connectBrowser()`. Use
`channel` (`chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`,
Expand All @@ -369,6 +370,7 @@ a managed directory under `~/.browser-commander/real-browser/`, rejects known
default browser-profile paths, protects its remote-debugging arguments, and
returns the spawned `browserProcess`, resolved `cdpEndpoint`, executable path,
and profile path alongside `{ browser, page }`.
`launchAndConnectRealBrowser()` is an alias with identical behavior.

### saveStorageState(page, filePath)

Expand Down
9 changes: 9 additions & 0 deletions js/src/browser/real-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@ export async function launchAndConnectRealBrowser(options = {}) {
return await launchAndConnectRealBrowserWithDependencies(options);
}

/**
* Short Playwright-style name for {@link launchAndConnectRealBrowser}.
*
* Both names are the same function so existing callers can keep using the
* descriptive name while new code can use the API proposed for real-browser
* launch.
*/
export const launchRealBrowser = launchAndConnectRealBrowser;

/** Dependency-injected implementation used by the public helper and tests. */
export async function launchAndConnectRealBrowserWithDependencies(
options = {},
Expand Down
5 changes: 4 additions & 1 deletion js/src/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export {

// Re-export browser management
export { connectBrowser } from './browser/connector.js';
export { launchAndConnectRealBrowser } from './browser/real-browser.js';
export {
launchAndConnectRealBrowser,
launchRealBrowser,
} from './browser/real-browser.js';
export { launchBrowser } from './browser/launcher.js';
export { saveStorageState } from './browser/storage-state.js';
export { emulateMedia } from './browser/media.js';
Expand Down
8 changes: 7 additions & 1 deletion js/tests/unit/browser/real-browser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import {
buildRealBrowserArgs,
launchAndConnectRealBrowser,
launchAndConnectRealBrowserWithDependencies,
launchRealBrowser,
} from '../../../src/browser/real-browser.js';
import { launchAndConnectRealBrowser as publicHelper } from '../../../src/index.js';
import {
launchAndConnectRealBrowser as publicHelper,
launchRealBrowser as publicShortHelper,
} from '../../../src/index.js';

describe('launchAndConnectRealBrowser', () => {
let temporaryDirectory;
Expand All @@ -24,6 +28,8 @@ describe('launchAndConnectRealBrowser', () => {

it('is exported from the package API', () => {
assert.equal(publicHelper, launchAndConnectRealBrowser);
assert.equal(launchRealBrowser, launchAndConnectRealBrowser);
assert.equal(publicShortHelper, launchRealBrowser);
});

it('builds a loopback-only CDP command with a dedicated profile', () => {
Expand Down
30 changes: 30 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,36 @@ non-default `--user-data-dir`; remote debugging is intentionally disabled for
the default Chrome profile. Cookie seeding uses only values supplied by the
caller and does not read the default profile.

### launch_real_browser(options)

Discover and start a genuine installed Chrome, Edge, Brave, or Chromium with a
dedicated profile, wait for its loopback CDP endpoint, and attach with
Playwright or Selenium:

```python
from browser_commander import RealBrowserOptions, launch_real_browser

result = await launch_real_browser(
RealBrowserOptions(
engine="playwright", # or "selenium"
channel="chrome", # chrome, msedge, brave, or chromium
user_data_dir="/tmp/browser-commander-profile",
seed_cookies=[
{"name": "session", "value": "saved", "url": "https://example.com"}
],
)
)

browser, page = result.browser, result.page
print(result.cdp_endpoint, result.executable_path)
```

The helper also supports beta/dev/canary channels and an explicit
`executable_path`. It rejects known default browser profiles and prevents
custom arguments from overriding its loopback address, debugging port, or
profile. The returned `browser_process` can be terminated explicitly after
closing the browser. `launch_and_connect_real_browser()` is an alias.

The `color_scheme` option emulates `prefers-color-scheme` at launch time:

```python
Expand Down
3 changes: 3 additions & 0 deletions python/changelog.d/68.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- Added `launch_real_browser()` for discovering and starting an installed Chrome-family browser with a dedicated profile before attaching over CDP.
8 changes: 8 additions & 0 deletions python/src/browser_commander/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
# Page trigger system
PageTriggerManager,
PlaywrightAdapter,
RealBrowserOptions,
RealBrowserResult,
ScrollResult,
ScrollVerificationResult,
SeleniumAdapter,
Expand Down Expand Up @@ -83,7 +85,9 @@
is_verbose_enabled,
# Element visibility
is_visible,
launch_and_connect_real_browser,
launch_browser,
launch_real_browser,
locator,
log_element_info,
make_url_condition,
Expand Down Expand Up @@ -149,6 +153,8 @@
# Page trigger system
"PageTriggerManager",
"PlaywrightAdapter",
"RealBrowserOptions",
"RealBrowserResult",
"ScrollResult",
"ScrollVerificationResult",
"SeleniumAdapter",
Expand Down Expand Up @@ -195,7 +201,9 @@
"is_verbose_enabled",
# Element visibility
"is_visible",
"launch_and_connect_real_browser",
"launch_browser",
"launch_real_browser",
"locator",
"log_element_info",
"make_browser_commander",
Expand Down
10 changes: 10 additions & 0 deletions python/src/browser_commander/browser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,29 @@
wait_for_url_stabilization,
)
from browser_commander.browser.pdf import pdf
from browser_commander.browser.real_browser import (
RealBrowserOptions,
RealBrowserResult,
launch_and_connect_real_browser,
launch_real_browser,
)

__all__ = [
"ConnectOptions",
"GotoResult",
"LaunchOptions",
"LaunchResult",
"NavigationVerificationResult",
"RealBrowserOptions",
"RealBrowserResult",
"WaitAfterActionResult",
"connect_browser",
"default_navigation_verification",
"emulate_media",
"goto",
"launch_and_connect_real_browser",
"launch_browser",
"launch_real_browser",
# PDF generation
"pdf",
"verify_navigation",
Expand Down
Loading
Loading