CLI tool to control Chrome via DevTools Protocol. No MCP, no Puppeteer, no intermediaries. Talks directly to Chrome.
go install github.com/scorredoira/bro@latestSingle session (most common):
bro open http://localhost:9092/admin/login
bro fill Email admin@demo.com
bro fill Password 123
bro click Login
bro closeMultiple parallel sessions (each gets its own isolated Chrome):
PORT=$(bro open http://localhost:9092/admin/login)
bro --port $PORT fill Email admin@demo.com
bro --port $PORT click Login
bro --port $PORT closebro [--port PORT] [--headless] [-w N] <command> [args...]
Default port: 9222. Use --headless for headless mode (no browser window). When running multiple sessions, bro open auto-picks the next free port and prints it.
Launch a new Chrome instance and optionally navigate to a URL. Prints the port number to stdout. Each instance is fully isolated (separate profile, cookies, storage).
bro open # launch Chrome, print port
bro open http://localhost:9092/admin/login # launch and navigate
bro --headless open http://example.com # headless mode (no window)If a Chrome is already running on the default port (or the one specified with --port), prints that port.
Works on macOS, Linux, and Windows. Auto-detects Chrome location.
Kill the Chrome instance on the given port.
bro close # close Chrome on default port 9222
bro --port 9223 close # close Chrome on port 9223Go to a URL. Waits for the page to load.
bro --port $PORT navigate http://localhost:9092/admin/login
bro --port $PORT nav http://example.comReload the current page.
bro --port $PORT reloadGo back in browser history.
bro --port $PORT backGo forward in browser history.
bro --port $PORT forwardResize the browser window.
bro --port $PORT resize 1280 720
bro --port $PORT resize 375 812 # iPhone X
bro --port $PORT resize 1920 1080 # Full HDPrint the accessibility tree. This is how you find elements to interact with.
bro --port $PORT snapshot
bro --port $PORT snap --verboseOutput:
[4] RootWebArea "Login"
[12] textbox "Email" value=""
[15] textbox "Password" value=""
[18] button "Login"
Use the text shown in quotes to target elements with click, fill, etc.
Take a screenshot. Defaults to /tmp/bro.png.
bro --port $PORT screenshot
bro --port $PORT ss /tmp/login-page.png
bro --port $PORT screenshot --full /tmp/full-page.pngOptions:
--full— capture the entire page (scroll included)
Print the current page URL.
bro --port $PORT urlPrint the page HTML source.
bro --port $PORT html
bro --port $PORT html > /tmp/page.htmlClick an element by its visible text. Also supports --css and --id flags for DOM-based lookup.
bro --port $PORT click Login
bro --port $PORT click "Save changes"
# Click by CSS selector (useful for elements not in the accessibility tree)
bro --port $PORT click --css ".grid-cell" "08:36"
bro --port $PORT click --css ".btn-primary"
# Click by DOM id
bro --port $PORT click --id submitBtnWhen using --css or --id, an optional text argument filters by text content (case-insensitive substring match).
Double-click an element. Supports --css and --id flags.
bro --port $PORT dblclick "row content"
bro --port $PORT dblclick --css ".editable-cell" "Total"Fill an input field. First argument is the label text, rest is the value. Matches by accessible name: <label>, aria-label, or placeholder text.
bro --port $PORT fill Email admin@demo.com
bro --port $PORT fill Password 123
bro --port $PORT fill "First name" SantiagoZero-width characters in placeholder text are stripped automatically for matching.
Select a dropdown option. Works with native <select> and custom widget dropdowns. For custom widgets, it clicks to open the dropdown, waits for options to render, then clicks the matching option.
bro --port $PORT select Country Spain
bro --port $PORT select "Leave type" Vacation
# Works with custom widget dropdowns (e.g. React Select, S.Select)
bro --port $PORT select "Booking type" "Green Fee 18"If the dropdown trigger doesn't have a standard input role (textbox, combobox), select falls back to finding it by visible text.
Type raw text into the currently focused element.
bro --port $PORT type "hello world"Press a keyboard key.
bro --port $PORT press Enter
bro --port $PORT press Tab
bro --port $PORT press EscapeSupported keys: Enter, Tab, Escape/Esc, Backspace, Delete, ArrowUp/Up, ArrowDown/Down, ArrowLeft/Left, ArrowRight/Right, Space, Home, End, PageUp, PageDown.
Hover over an element. Supports --css and --id flags.
bro --port $PORT hover "Settings"
bro --port $PORT hover --css ".menu-item" "Reports"Drag one element to another.
bro --port $PORT drag "Item 1" "Drop zone"Upload a file to a file input. Uses a CSS selector (not text).
bro --port $PORT upload "input[type=file]" /path/to/document.pdfWait for text to appear on the page (default timeout: 10s).
bro --port $PORT wait Dashboard
bro --port $PORT wait "Record saved"
bro --port $PORT wait --timeout 30s "Processing complete"Wait for text to disappear.
bro --port $PORT wait --gone "Loading..."Wait for the URL to contain a pattern.
bro --port $PORT wait --url /admin/dashboardList all open tabs.
bro --port $PORT pagesSwitch to a tab by its index.
bro --port $PORT page 0Open a new tab.
bro --port $PORT newpage http://localhost:9092/adminClose the current tab.
bro --port $PORT closepageEvaluate arbitrary JavaScript. Promises are automatically awaited.
bro --port $PORT js "document.title"
bro --port $PORT js "document.querySelectorAll('button').length"
# Promises are awaited automatically
bro --port $PORT js "fetch('/api/status').then(r => r.json())"Show captured console messages. On first call, installs a capture hook.
bro --port $PORT console # installs capture
# ... do something ...
bro --port $PORT console # shows messagesShow recent network requests.
bro --port $PORT networkAccept the next JavaScript dialog.
bro --port $PORT dialog accept
bro --port $PORT dialog accept "prompt text"Dismiss the next dialog.
bro --port $PORT dialog dismissNote: Call dialog before triggering the action that opens the dialog.
Run .bro test files. Each test launches its own isolated Chrome, executes commands in order, and reports pass/fail.
bro test tests/ # run all .bro files in directory (recursive)
bro test tests/login.bro # run a single test
bro --headless test tests/ # headless mode (no browser window)
bro -w 4 --headless test tests/ # 4 tests in parallelTest files use the .bro extension. Each file is a sequence of bro commands, one per line:
# Login with valid credentials
open http://localhost:9092/admin/login
fill Email admin@demo.com
fill Password 123
click Login
assert url /admin/dashboard
assert text Dashboard
assert gone "Invalid credentials"
Rules:
- Lines starting with
#are comments — the first one is the test name - Blank lines are ignored
- Each line is a bro command (same syntax as the CLI)
assertcommands verify conditions with automatic retry (default timeout: 10s)
| Command | What it checks |
|---|---|
assert url <pattern> |
URL contains pattern |
assert text <text> |
Text is visible on the page |
assert gone <text> |
Text is NOT on the page |
assert title <text> |
Page title contains text |
assert js <expression> |
JavaScript expression returns truthy |
Assertions retry automatically until they pass or timeout. No need for explicit waits or sleeps between actions and assertions.
Override the default 10s timeout:
click "Generate report"
assert --timeout 30s text "Report ready"
start ensures a server is running before the test continues. If the port is already open, it's a no-op. Otherwise it launches the command and waits up to 30s for the port to accept HTTP connections.
start :3000 node server.js
start :9092 go run ./cmd/server
open http://localhost:3000
exec runs a shell command inside a test. Stdout is captured in ${result} (trimmed).
# Read a token from the database and use it
exec mysql -N -s -e "SELECT token FROM s_main.accounts WHERE email='user@test.com'"
navigate http://localhost:9092/reset-password?token=${result}
Rules:
- Fails the test if exit code ≠ 0
- Stdout is always captured in
${result}(overwrites previous value) ${result}is expanded in all subsequent lines- Use
exec --as VARNAMEfor named variables:exec --as TOKEN mysql ...→${TOKEN} execruns before Chrome is launched — useful for setup (creating users, cleaning DB)
Login flow:
# Login with valid credentials
open http://localhost:9092/admin/login
fill Email admin@demo.com
fill Password 123
click Login
assert url /admin/dashboard
assert text Dashboard
Failed login:
# Login with wrong password shows error
open http://localhost:9092/admin/login
fill Email admin@demo.com
fill Password wrong
click Login
assert url /admin/login
assert text "Invalid credentials"
assert gone Dashboard
Form submission:
# Create a new user
open http://localhost:9092/admin/users
click "New user"
assert url /admin/users/new
fill "First name" Santiago
fill "Last name" Test
fill Email test@example.com
select Role Admin
click Save
assert text "User created"
JavaScript assertion:
# Page has no console errors
open http://localhost:9092/admin/dashboard
assert js "document.querySelectorAll('.error').length === 0"
assert js "document.title.includes('Dashboard')"
PASS login_ok.bro — Login with valid credentials (1.2s)
PASS create_user.bro — Create a new user (3.4s)
FAIL delete_record.bro:12 — assert text: "Record deleted" not found
3 tests, 2 passed, 1 failed (4.8s)
Each bro open launches a separate Chrome instance with its own port and profile directory. Sessions are fully isolated — no shared cookies, localStorage, or state.
# Terminal 1
PORT=$(bro open http://localhost:9092/admin/login)
bro --port $PORT fill Email admin@demo.com
bro --port $PORT click Login
bro --port $PORT close
# Terminal 2 (runs in parallel, completely isolated)
PORT=$(bro open http://localhost:9092/admin/login)
bro --port $PORT fill Email other@demo.com
bro --port $PORT click Login
bro --port $PORT closebro openlaunches Chrome with--remote-debugging-portand a unique--user-data-dirper port- All commands connect via WebSocket to Chrome's DevTools Protocol, act, and exit
- Element lookup uses Chrome's accessibility tree by default — reliable with any framework
--cssand--idflags use DOM selectors as an alternative for elements not in the AX tree- Interactive elements (buttons, links) are prioritized over static text when names match
- Zero-width characters in element names/placeholders are stripped for robust matching
bro jsusesRuntime.evaluatewithawaitPromiseso Promises resolve automaticallybro closesends a close command via CDP to cleanly shut down the Chrome instance
Built on Rod.