browser://¶
Producer-only browser automation — drive a live web app in headless
Chromium from a declarative JSON action list, read the rendered (post-JS)
DOM, interact, evaluate JS, capture console, and assert. This is the piece
image://?op=screenshot can't do: that rasterises a static HTML string (a
data: URL, one-shot); browser:// navigates to a real http(s):// / file://
URL, waits for it to hydrate, and drives it — the substrate for E2E / UI
testing, visual regression, and scraping JS-rendered pages.
One operation:
op=run(default) — the exchange body is a JSON action list; the steps run in order against one shared page and return a structured result JSON. Any failedassert*(or a timeout / missing selector) makes the op return an error, so the pipeline fails — a real "test failed" signal composable withthrow-exception/failIf.
The headless-Chromium engine (via chromedp)
is shared with pdf:// and image:// through
pkg/component/internal/chromerender — one browser-launch path. Requires a
chromium / google-chrome binary on PATH (or chromePath) at runtime.
Producer-only — CreateConsumer returns ErrNoConsumer.
When to use which¶
| You have… | …and want to | Use |
|---|---|---|
| a live URL (SPA / hydrated app) | navigate, interact, assert on the rendered DOM | browser://run |
| an HTTP API | assert status / body bytes | http:// + failIf |
| a static HTML string | rasterise to PNG/JPEG | image://?op=screenshot |
| a live URL | just a screenshot of the rendered page | browser://run with a single screenshot action |
| HTML | a PDF | pdf://?mode=chrome |
image://?op=screenshot cannot screenshot a live URL (it only loads a data:
URL of an HTML body); use a browser:// goto + screenshot for that.
URI shape¶
Session parameters¶
| Param | Type | Default | Description |
|---|---|---|---|
op |
enum | run |
Only run today: execute the action-list body. |
timeout |
duration | 30s |
Overall session timeout covering all actions. |
width |
integer | 1280 |
Viewport width in CSS px. |
height |
integer | 800 |
Viewport height in CSS px. |
deviceScaleFactor |
number | 1 |
Device pixel ratio; 2 = retina / 2× screenshots. |
console |
boolean | false |
Collect page console logs into result.console. |
chromePath |
string | autodiscover | Explicit Chromium / Chrome binary path. |
userAgent |
string | — | Override the session User-Agent. |
ignoreHTTPSErrors |
boolean | false |
Ignore TLS cert errors (self-signed / local dev). |
Body: the action list¶
The body is a JSON array of {action, …} objects — a bare array or an
object with an actions key. It may arrive as JSON bytes / a string / an
io.Reader, or as a Starlang-produced structure (a set-body starlang block
that yields a list of maps). Steps run in order, sharing one page.
| action | fields | effect |
|---|---|---|
goto |
url, waitUntil: load\|domcontentloaded\|networkidle |
navigate a live URL |
waitForSelector |
selector, state: visible\|attached\|hidden, timeout |
wait for an element |
waitForTimeout |
ms |
fixed wait |
click |
selector |
click |
fill |
selector, value |
set an input value (fires input/change) |
type |
selector, text |
send real keystrokes |
select |
selector, value |
choose an <option> (fires change) |
press |
selector?, key |
keyboard key (Enter, Tab, ArrowDown, …) |
hover |
selector |
dispatch hover events at the element |
getText |
selector, as |
element text → returns[as] |
getAttribute |
selector, name, as |
attribute value → returns[as] |
queryAll |
selector, fields:[text,href,type,name,id,value,class,visible], as |
element list → returns[as] |
evaluate |
expression, as |
run JS, JSON result → returns[as] |
content |
as |
full rendered HTML → returns[as] |
screenshot |
path (or name), fullPage |
write a PNG of the live page |
assertText |
selector? (default body), contains |
fail the op if rendered text lacks contains |
assertVisible |
selector, timeout |
fail if not visible |
assertURL |
contains |
fail if the current URL lacks contains |
Result¶
The op writes a structured result JSON to the body (Content-Type:
application/json), on ex.In() too so the next step sees it:
{
"ok": true,
"url": "http://localhost:5173/",
"steps": [ { "action": "goto", "ok": true, "value": "http://localhost:5173/" }, … ],
"returns": { "count": 12, "title": "My App" },
"assertions": [ { "desc": "text of \"#app\" contains \"Ready\"", "pass": true } ],
"console": [ { "type": "log", "text": "hydrated 42" } ],
"screenshots": [ "/var/out/home.png" ]
}
On failure, ok is false, error holds the message, steps records progress
up to the failing action — and the op returns an error, failing the route.
Headers (written)¶
| Header | Description |
|---|---|
Content-Type |
application/json. |
browser.ok |
true / false. |
browser.steps |
Number of steps executed. |
browser.screenshots |
Comma-separated screenshot paths (if any). |
Examples¶
Smoke-test a live SPA — read the rendered DOM and fail the pipeline if a post-hydration marker is missing:
workflows:
- id: smoke
trigger: { uri: http-server://0.0.0.0:8080/smoke }
steps:
- set-body:
starlang: |
[
{"action":"goto", "url":"http://localhost:5173/", "waitUntil":"networkidle"},
{"action":"assertText", "selector":"#app", "contains":"Ready"},
{"action":"screenshot", "path":"/var/out/home.png"}
]
- target: browser://run?console=true
Drive a login form and assert the result:
- set-body:
starlang: |
[
{"action":"goto", "url":"http://localhost:5173/login", "waitUntil":"networkidle"},
{"action":"fill", "selector":"#email", "value":"me@example.com"},
{"action":"fill", "selector":"#password", "value":"secret"},
{"action":"click", "selector":"button[type=submit]"},
{"action":"assertURL", "contains":"/dashboard"},
{"action":"assertText", "selector":".welcome", "contains":"Welcome"}
]
- target: browser://run?timeout=45s
Scrape a JS-rendered list (post-hydration), returning structured data:
- set-body:
starlang: |
[
{"action":"goto", "url":"http://localhost:5173/products", "waitUntil":"networkidle"},
{"action":"queryAll", "selector":".card .title", "fields":["text","href"], "as":"products"},
{"action":"evaluate", "expression":"document.querySelectorAll('.card').length", "as":"count"}
]
- target: browser://run
# result.returns.products / result.returns.count
Snapshot a live URL (which image://?op=screenshot cannot do):
- set-body:
starlang: |
[
{"action":"goto", "url":"https://example.com/", "waitUntil":"networkidle"},
{"action":"screenshot", "path":"/var/out/live.png", "fullPage":true}
]
- target: browser://run?width=1440&height=900&deviceScaleFactor=2
Behavior notes¶
- Reads the rendered DOM.
getText/queryAll/content/assertTextsee the post-JS DOM, so they test dynamic SPAs — not the served bytes. waitUntil: networkidlewaits until no network request has been in flight for ~500ms (bounded by the actiontimeout, default 15s). It's the SPA-hydration wait;load(default) just waits for the load event. Prefer an explicitwaitForSelectorwhen a specific element marks readiness.fill/selectset the value and dispatchinput/change, so framework listeners (React/Vue/…) react;typesends real keystrokes.hoverdispatchesmouseover/mouseenter/mousemoveat the element centre — enough to trigger hover state / menus.screenshotwrites topath(a session may take several), and the paths are listed inresult.screenshots. UsefullPagefor the whole scrollable page;deviceScaleFactorcontrols resolution.- A failed assertion / timeout / missing selector returns an error →
the route fails. Compose with
throw-exception/failIf, or catch it in an error handler; the result JSON is still on the body for inspection. - Shared engine —
browser://runs the samechromerenderlaunch path aspdf://mode=chromeandimage://op=screenshot; one Chromium binary, one setup.
Source¶
pkg/component/browser/browser.go— component, endpoint, params, dispatch.pkg/component/browser/session.go— action interpreter, result assembly, networkidle + console capture.pkg/component/internal/chromerender/render.go— shared headless-Chromium session (WithSession).
See also¶
- image — HTML string → image (
op=screenshot); PDF page → image (op=rasterize). - pdf — HTML → PDF (
mode=chrome) and structured → PDF (mode=native). - http — API-level requests + status/body assertions (no browser).
- template — build HTML to serve to the app under test.
- file, s3 — sinks for screenshots and scraped data.