Web Platform
maestro-runner can test web applications directly in a Chromium browser using the Chrome DevTools Protocol (CDP). No Selenium, no WebDriver, no external dependencies — just a single binary.
Quick Start
# Run a flow against a web app
maestro-runner --platform web flows/login.yaml
# With a visible browser window
maestro-runner --platform web --headed flows/
# Use your installed Chrome instead of bundled Chromium
maestro-runner --platform web --browser chrome flows/
Your flow file uses url (or appId) to set the web app URL:
url: https://myapp.example.com
name: Login Flow
---
- launchApp
- tapOn: "Sign In"
- inputText:
text: "[email protected]"
placeholder: "Email"
- inputText:
text: "password123"
placeholder: "Password"
- tapOn: "Log In"
- assertVisible: "Welcome back"
On first run, maestro-runner downloads a bundled Chromium (~130MB) to ~/.maestro-runner/browsers/. Subsequent runs reuse the cached binary.
CLI Flags (Web-Specific)
| Flag | Type | Default | Env Var | Description |
|---|---|---|---|---|
--platform web |
string | MAESTRO_PLATFORM |
Required. Selects the web/browser driver | |
--headed |
bool | false |
Show the browser window (default is headless) | |
--browser |
string | chromium |
MAESTRO_BROWSER |
Browser to use: chrome, chromium, or path to binary |
--parallel |
int | Run N browser instances in parallel |
Browser Selection
| Value | Behavior |
|---|---|
chromium (default) |
Downloads and uses Rod's bundled Chromium |
chrome |
Uses locally installed Google Chrome (falls back to Chromium if not found) |
/path/to/binary |
Uses a custom browser binary at the given path |
# Bundled Chromium (default)
maestro-runner --platform web flows/
# Installed Chrome
maestro-runner --platform web --browser chrome flows/
# Custom binary
maestro-runner --platform web --browser /opt/chrome-canary/chrome flows/
Flow Configuration
The config section (above the --- separator) controls flow-level settings.
URL / App ID
Use url or appId to set the web app URL. This URL is used by launchApp.
url: https://staging.myapp.com
name: Checkout Flow
tags:
- smoke
env:
API_KEY: test-key-123
---
- launchApp
Both url and appId work — they're interchangeable for web flows:
appId: https://staging.myapp.com
Or set the URL directly on the launchApp step:
- launchApp: https://staging.myapp.com
Config Fields
| Field | Type | Description |
|---|---|---|
url |
string | Web app URL (used by launchApp) |
appId |
string | Alias for url — both work on web |
name |
string | Flow name (shown in reports) |
tags |
list | Tags for filtering flows |
env |
map | Environment variables (accessible as ${VAR_NAME} in steps) |
timeout |
int | Flow timeout in ms (entire flow) |
commandTimeout |
int | Default timeout for all commands in ms |
Lifecycle Hooks
Run setup/teardown steps automatically:
url: https://myapp.example.com
onFlowStart:
- evalBrowserScript: "localStorage.setItem('debug', 'true')"
onFlowComplete:
- clearConsoleLogs
---
- launchApp
- tapOn: "Submit"
onFlowStart runs before any flow steps. onFlowComplete runs after all steps (even on failure).
Common Step Properties
Every step supports these optional fields:
| Field | Type | Description |
|---|---|---|
optional |
bool | If true, step failure doesn't fail the flow (timeout: 7s instead of 17s) |
timeout |
int | Step-specific timeout in ms (overrides flow's commandTimeout) |
label |
string | Human-readable label shown in reports |
# Optional step — won't fail the flow if element is missing
- tapOn:
text: "Dismiss"
optional: true
# Custom timeout — wait up to 30 seconds
- assertVisible:
text: "Report generated"
timeout: 30000
# Label for reports
- tapOn:
text: "Submit"
label: "Submit the order form"
Supported Commands
These commands work on web just as they do on Android/iOS. See Flow Commands for the full reference — below are web-specific examples.
Navigation
# Navigate to URL (uses flow config url, or specify directly)
- launchApp
- launchApp: https://myapp.com
# Navigate with clear state (clears cookies, storage, cache)
- launchApp:
appId: https://myapp.com
clearState: true
# Navigate to a URL
- openLink: "https://example.com/page"
- openBrowser: "https://example.com/page"
# Go back in browser history
- back
launchApp navigates to the URL. stopApp and killApp navigate to about:blank. clearState clears cookies, localStorage, sessionStorage, and browser cache.
Tap & Click
# By text
- tapOn: "Login"
# By any selector
- tapOn:
testId: submit-btn
- tapOn:
css: "button[type=submit]"
- tapOn:
placeholder: "Search"
- tapOn:
role: button
- tapOn:
href: "/settings"
# Double click
- doubleTapOn: "Item"
# Long press (mouse down 1s, mouse up)
- longPressOn: "Delete"
# Click at coordinates
- tapOnPoint:
x: 100
y: 200
Text Input
# Type into focused element
- inputText: "[email protected]"
# Type into element by any selector
- inputText:
text: "[email protected]"
placeholder: "Email"
- inputText:
text: "search query"
css: "input[type=search]"
# Erase characters (default: 50 if no count specified)
- eraseText
- eraseText: 10
hideKeyboard is a no-op on web (no virtual keyboard).
Scroll & Swipe
# Scroll (dispatches mouse wheel events at viewport center)
- scroll: DOWN
- scroll: UP
# Scroll until element visible (max 10 scrolls)
- scrollUntilVisible:
element: "Footer"
direction: DOWN
# Swipe (click-and-drag)
- swipe: LEFT
- swipe: RIGHT
Assertions
# Visible text
- assertVisible: "Welcome"
# Visible element by selector
- assertVisible:
testId: success-banner
- assertVisible:
css: ".notification"
# Not visible
- assertNotVisible: "Error"
# Assert condition (script-based)
- assertTrue: "${totalPrice > 0}"
assertNotVisible uses a browser-side requestAnimationFrame polling loop for fast resolution (~16ms detection).
Wait Commands
# Wait for element to appear
- extendedWaitUntil:
visible:
text: "Ready"
timeout: 30000
# Wait for DOM to stabilize (animations to end)
- waitForAnimationToEnd
waitForAnimationToEnd waits for the DOM to stop changing (300ms stability threshold, 10s max).
Other Shared Commands
# Keyboard
- pressKey: ENTER
- pressKey: TAB
- pressKey: ESCAPE
# Alerts & Dialogs
- acceptAlert
- dismissAlert
# Device Emulation
- setLocation:
latitude: "37.7749"
longitude: "-122.4194"
# Rotate viewport (swaps width/height)
- setOrientation: LANDSCAPE
# Clipboard
- copyTextFrom: "Price"
- pasteText
# Screenshots
- takeScreenshot: "login-screen.png"
# Variables
- defineVariables:
env:
BASE_URL: "https://staging.myapp.com"
# Flow Control
- runFlow: "flows/login.yaml"
- repeat:
times: "3"
commands:
- scroll: DOWN
Parallel Browser Execution
Run tests across multiple browser instances simultaneously:
# Run with 3 parallel browsers
maestro-runner --platform web --parallel 3 flows/
# Run with 5 parallel browsers, headed mode
maestro-runner --platform web --parallel 5 --headed flows/
Each parallel browser is a separate Chromium process with its own profile — full isolation, no shared state. The browser binary is downloaded once (if needed) before launching workers.
Flows are distributed across browsers using the same work-stealing scheduler as mobile parallel execution.
Unsupported Commands
These mobile-only commands are not available on the web platform:
| Command | Reason |
|---|---|
setAirplaneMode / toggleAirplaneMode |
Use setNetworkConditions: { offline: true } instead |
travel |
No GPS simulation in browser (use setLocation for static location) |
addMedia |
No camera/photo library in browser |
startRecording / stopRecording |
No screen recording |
clearKeychain |
No keychain in browser (use clearState) |
setPermissions |
Use grantPermissions / resetPermissions instead |
assertNoDefectsWithAI / assertWithAI / extractTextWithAI |
Not yet supported on web |
Troubleshooting
Browser download on first run
On first run, maestro-runner downloads a bundled Chromium browser (~130MB). This is one-time — subsequent runs use the cached binary at ~/.maestro-runner/browsers/. To use your existing Chrome installation instead:
maestro-runner --platform web --browser chrome flows/
Element not found
The web driver finds elements using a multi-stage cascade (CSS > attribute > role > ID > text). If an element is not found:
- Use a precise selector —
css,testId, ornameare the most reliable. Text selectors search the AX tree which may not match what you see on screen. - Select by type attribute — use CSS:
css: "input[type=email]",css: "button[type=submit]". - Wait for the page — add
- waitForAnimationToEndbefore the assertion if the page is still loading. - Check visibility — elements with
display: none,visibility: hidden, or zero dimensions are not findable. - Check nth — if multiple elements match, use
nth: 0,nth: 1, etc. to pick a specific one. - Use headed mode — run with
--headedto see what the browser sees.
Selector priority
If you set multiple selector fields on the same step, only one is used (first match wins in this order):
css > testId > name > placeholder > href > alt > title > role > id > text > textContains > textRegex
Headed mode for debugging
Run with --headed to see the browser window and watch your test execute:
maestro-runner --platform web --headed flows/login.yaml
Console errors
Use assertNoJSErrors to catch silent JavaScript errors. To debug, add getConsoleLogs and inspect the output:
- getConsoleLogs: logs
- evalBrowserScript:
script: |
// logs are now in a variable, use evalScript to inspect
return "check logs variable"
Unsupported selector warning
If you use a selector field that's not supported on web (e.g., traits, width, height, tolerance, index), the driver logs a warning and ignores that field. The step still executes using the remaining selector fields.