Skip to content

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 failed assert* (or a timeout / missing selector) makes the op return an error, so the pipeline fails — a real "test failed" signal composable with throw-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

browser://run?timeout=30s&width=1280&height=800&deviceScaleFactor=2&console=true

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 / assertText see the post-JS DOM, so they test dynamic SPAs — not the served bytes.
  • waitUntil: networkidle waits until no network request has been in flight for ~500ms (bounded by the action timeout, default 15s). It's the SPA-hydration wait; load (default) just waits for the load event. Prefer an explicit waitForSelector when a specific element marks readiness.
  • fill / select set the value and dispatch input/change, so framework listeners (React/Vue/…) react; type sends real keystrokes.
  • hover dispatches mouseover/mouseenter/mousemove at the element centre — enough to trigger hover state / menus.
  • screenshot writes to path (a session may take several), and the paths are listed in result.screenshots. Use fullPage for the whole scrollable page; deviceScaleFactor controls 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 same chromerender launch path as pdf://mode=chrome and image://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.