Headless Browser (browser)
Experimental (1.25.0). Drives a headless Chrome/Chromium over the Chrome DevTools Protocol for functional/E2E testing and scripted browser control. The surface may change.
The browser module launches a real headless browser and lets a Geblang program
navigate, interact with the page, read content, take screenshots, manage cookies,
and intercept requests. It speaks the DevTools Protocol directly (no external
driver), is gated behind the --allow-browser launch flag (it spawns a browser
subprocess and opens sockets), and always terminates the browser on close so a
process is never orphaned.
Setup
Install Chrome or Chromium yourself; it is not bundled. The module finds it via
opts.executable, then $GEBLANG_CHROME, then common install locations
(google-chrome, chromium, ...). Run with --allow-browser:
export GEBLANG_CHROME=/usr/bin/google-chrome # or pass {"executable": "..."}
geblang --allow-browser script.gb
In containers Chrome usually needs --no-sandbox; pass it through opts.args.
Without --allow-browser, browser.launch raises a PermissionError.
Launching
browser.launch(opts = {}) returns a Browser. opts: headless (default
true), executable, args (list of extra Chrome flags), timeoutMs.
Browser method |
Description |
|---|---|
newPage() |
Opens a new page/tab; returns a Page. |
pages() |
All open pages/tabs (including ones the app opened). |
version() |
The browser product version string. |
close() |
Closes the browser and terminates the process. |
Pages
import browser;
import io;
let b = browser.launch({"args": ["--no-sandbox"]}); # geblang --allow-browser
let p = b.newPage();
p.goto("https://example.com");
p.waitFor("h1");
io.println(p.title());
io.println(p.text("h1"));
io.println(p.evaluate("document.querySelectorAll('a').length"));
p.screenshot("example.png");
b.close();
Page method |
Description |
|---|---|
goto(url) |
Navigates and waits for load. |
waitFor(selector, timeoutMs = 30000) |
Waits until a selector matches. |
click(selector) |
Scrolls to and clicks with real mouse events. |
type(selector, text) |
Focuses the element and types text. |
fill(selector, value) |
Sets a form field's value and fires input/change. |
press(key) |
Presses a key, e.g. "Enter", "Tab". |
select(selector, value) |
Selects a dropdown option by value. |
evaluate(js) |
Runs JavaScript and returns the JSON-serializable result. |
text(selector) / attribute(selector, name) |
Read an element's text / attribute (or null). |
content() / title() / url() |
The page HTML / document title / current URL. |
reload() |
Reloads and waits for load. |
screenshot(path) / pdf(path) |
Write a PNG screenshot / PDF to a file. |
cookies() / setCookie(cookie) / clearCookies() |
Read / set / clear cookies. |
route(urlPattern, handler) |
Intercept matching requests (see below). |
close() |
Closes the page. |
evaluate returns the JS value marshalled to a Geblang value; a JavaScript
exception surfaces as a catchable error.
Request interception
page.route(urlPattern, handler) intercepts every request whose URL matches the
pattern (* is the wildcard). The handler receives a request dict
(url, method, headers, resourceType) and returns one of:
null- let the request proceed unchanged;{"abort": true}- block the request;{"status": ..., "headers": {...}, "body": "..."}- fulfill it with a mock response.
p.route("*/api/*", func(dict<string, any> req): ?dict<string, any> {
if ((req["url"] as string).contains("/api/users")) {
return {"status": 200, "headers": {"Content-Type": "application/json"},
"body": "[{\"id\":1,\"name\":\"Ada\"}]"};
}
return null; # everything else proceeds normally
});
This makes it straightforward to stub a backend in a functional test or to block trackers/assets while scripting a page.
A functional test
import browser;
import test;
class LoginTest extends test.Test {
@test
func logsIn(): void {
let b = browser.launch({"args": ["--no-sandbox"]});
let p = b.newPage();
p.goto("https://app.example.com/login");
p.fill("#email", "[email protected]");
p.fill("#password", "hunter2");
p.click("button[type=submit]");
p.waitFor(".dashboard");
this.assertTrue((p.url() as string).contains("/dashboard"));
b.close();
}
}
Building a binary
A built binary (geblang build) carries the capability when baked in: declare
permissions: { browser: true } in geblang.yaml, or pass
geblang build --allow-browser. See Bundling. The end user
still needs Chrome installed on the machine that runs the binary.