# Bibbidi
<img src="assets/icon.png" alt="Bibbidi icon" width="120" align="right" />
**B**EAM **I**nterface to **B**rowsers with **BiDi** — a nod to the Fairy Godmother's spell in Disney's *Cinderella*.
Low-level Elixir implementation of the [W3C WebDriver BiDi Protocol](https://w3c.github.io/webdriver-bidi/).
Bibbidi is a **building-block library** — it gives you WebSocket connectivity,
command/response correlation, and event dispatch, but imposes no supervision
tree. You supervise `Bibbidi.Connection` processes yourself, exactly how you
want.
Designed for RPA frameworks, browser testing libraries, and anything else that
needs to talk BiDi to a browser.
## Installation
```elixir
def deps do
[
{:bibbidi, "~> 0.3.0"}
]
end
```
## Quick Start
Launch a browser with BiDi support (Firefox has native support):
```bash
firefox --headless --remote-debugging-port=9222
```
Then in IEx:
<!-- tabs-open -->
### Struct API
```elixir
alias Bibbidi.Connection
alias Bibbidi.Commands.BrowsingContext.{GetTree, Navigate, CaptureScreenshot}
alias Bibbidi.Commands.Script.{Evaluate, CallFunction}
alias Bibbidi.Commands.Session.Subscribe
# Connect to the BiDi WebSocket endpoint
{:ok, conn} = Connection.start_link(url: "ws://localhost:9222/session")
# Start a session
{:ok, _caps} = Bibbidi.Session.new(conn)
# Get the browsing context tree
{:ok, tree} = Connection.execute(conn, %GetTree{})
context = hd(tree["contexts"])["context"]
# Navigate to a page
{:ok, _nav} = Connection.execute(conn, %Navigate{
context: context,
url: "https://example.com",
wait: "complete"
})
# Evaluate JavaScript
{:ok, result} = Connection.execute(conn, %Evaluate{
expression: "document.title",
target: %{context: context},
await_promise: false
})
IO.inspect(result)
# => %{"type" => "success", "result" => %{"type" => "string", "value" => "Example Domain"}, ...}
# Call a function with arguments
{:ok, result} = Connection.execute(conn, %CallFunction{
function_declaration: "function(a, b) { return a + b; }",
target: %{context: context},
await_promise: false,
arguments: [%{type: "number", value: 3}, %{type: "number", value: 4}]
})
# Take a screenshot (returns base64-encoded PNG)
{:ok, screenshot} = Connection.execute(conn, %CaptureScreenshot{context: context})
File.write!("screenshot.png", Base.decode64!(screenshot["data"]))
# End the session
{:ok, _} = Bibbidi.Session.end_session(conn)
Connection.close(conn)
```
### Function API
```elixir
# Connect to the BiDi WebSocket endpoint
{:ok, conn} = Bibbidi.Connection.start_link(url: "ws://localhost:9222/session")
# Check server status
{:ok, status} = Bibbidi.Session.status(conn)
IO.inspect(status)
# => %{"ready" => true, "message" => ""}
# Start a session
{:ok, caps} = Bibbidi.Session.new(conn)
# Get the browsing context tree
{:ok, tree} = Bibbidi.Commands.BrowsingContext.get_tree(conn)
context = hd(tree["contexts"])["context"]
# Navigate to a page
{:ok, nav} = Bibbidi.Commands.BrowsingContext.navigate(conn, context, "https://example.com", wait: "complete")
# Evaluate JavaScript
{:ok, result} = Bibbidi.Commands.Script.evaluate(conn, "document.title", %{context: context}, false)
IO.inspect(result)
# => %{"type" => "success", "result" => %{"type" => "string", "value" => "Example Domain"}, ...}
# Call a function with arguments
{:ok, result} = Bibbidi.Commands.Script.call_function(
conn,
"function(a, b) { return a + b; }",
false,
%{context: context},
arguments: [%{type: "number", value: 3}, %{type: "number", value: 4}]
)
# Take a screenshot (returns base64-encoded PNG)
{:ok, screenshot} = Bibbidi.Commands.BrowsingContext.capture_screenshot(conn, context)
File.write!("screenshot.png", Base.decode64!(screenshot["data"]))
# End the session
{:ok, _} = Bibbidi.Session.end_session(conn)
Bibbidi.Connection.close(conn)
```
<!-- tabs-close -->
## Example Module
A copy-pasteable module showing common patterns:
<!-- tabs-open -->
### Struct API
```elixir
defmodule MyApp.Browser do
alias Bibbidi.{Connection, Session}
alias Bibbidi.Commands.BrowsingContext.{GetTree, Navigate}
alias Bibbidi.Commands.Script.Evaluate
def run do
{:ok, conn} = Connection.start_link(url: "ws://localhost:9222/session")
{:ok, _caps} = Session.new(conn)
try do
{:ok, tree} = Connection.execute(conn, %GetTree{})
context = hd(tree["contexts"])["context"]
# Navigate and wait for full page load
{:ok, _} = Connection.execute(conn, %Navigate{
context: context,
url: "https://example.com",
wait: "complete"
})
# Extract page title
{:ok, %{"result" => %{"value" => title}}} =
Connection.execute(conn, %Evaluate{
expression: "document.title",
target: %{context: context},
await_promise: false
})
# Extract all links
{:ok, %{"result" => %{"value" => links}}} =
Connection.execute(conn, %Evaluate{
expression: ~s|Array.from(document.querySelectorAll("a"), a => a.href)|,
target: %{context: context},
await_promise: false
})
%{title: title, links: links}
after
Session.end_session(conn)
Connection.close(conn)
end
end
end
```
### Function API
```elixir
defmodule MyApp.Browser do
alias Bibbidi.{Connection, Session}
alias Bibbidi.Commands.{BrowsingContext, Script}
def run do
{:ok, conn} = Connection.start_link(url: "ws://localhost:9222/session")
{:ok, _caps} = Session.new(conn)
try do
{:ok, tree} = BrowsingContext.get_tree(conn)
context = hd(tree["contexts"])["context"]
# Navigate and wait for full page load
{:ok, _} = BrowsingContext.navigate(conn, context, "https://example.com", wait: "complete")
# Extract page title
{:ok, %{"result" => %{"value" => title}}} =
Script.evaluate(conn, "document.title", %{context: context}, false)
# Extract all links
{:ok, %{"result" => %{"value" => links}}} =
Script.evaluate(
conn,
~s|Array.from(document.querySelectorAll("a"), a => a.href)|,
%{context: context},
false
)
%{title: title, links: links}
after
Session.end_session(conn)
Connection.close(conn)
end
end
end
```
<!-- tabs-close -->
## Listening for Events
<!-- tabs-open -->
### Struct API
```elixir
alias Bibbidi.Connection
alias Bibbidi.Commands.BrowsingContext.{GetTree, Navigate}
alias Bibbidi.Commands.Session.Subscribe
{:ok, conn} = Connection.start_link(url: "ws://localhost:9222/session")
{:ok, _} = Bibbidi.Session.new(conn)
# Tell the server to send browsingContext events
{:ok, _} = Connection.execute(conn, %Subscribe{events: ["browsingContext.load"]})
# Tell the connection to forward them to us
:ok = Connection.subscribe(conn, "browsingContext.load")
{:ok, tree} = Connection.execute(conn, %GetTree{})
context = hd(tree["contexts"])["context"]
# Navigate — this will trigger a load event
{:ok, _} = Connection.execute(conn, %Navigate{context: context, url: "https://example.com"})
# Receive the event
receive do
{:bibbidi_event, "browsingContext.load", params} ->
IO.puts("Page loaded: #{params["url"]}")
after
10_000 -> IO.puts("Timeout waiting for load event")
end
```
### Function API
```elixir
{:ok, conn} = Bibbidi.Connection.start_link(url: "ws://localhost:9222/session")
{:ok, _} = Bibbidi.Session.new(conn)
# Tell the server to send browsingContext events
{:ok, _} = Bibbidi.Session.subscribe(conn, ["browsingContext.load"])
# Tell the connection to forward them to us
:ok = Bibbidi.Connection.subscribe(conn, "browsingContext.load")
{:ok, tree} = Bibbidi.Commands.BrowsingContext.get_tree(conn)
context = hd(tree["contexts"])["context"]
# Navigate — this will trigger a load event
{:ok, _} = Bibbidi.Commands.BrowsingContext.navigate(conn, context, "https://example.com")
# Receive the event
receive do
{:bibbidi_event, "browsingContext.load", params} ->
IO.puts("Page loaded: #{params["url"]}")
after
10_000 -> IO.puts("Timeout waiting for load event")
end
```
<!-- tabs-close -->
## Supervision
Bibbidi doesn't impose a process tree. Add connections to your own supervisor:
```elixir
children = [
{Bibbidi.Connection, url: "ws://localhost:9222/session", name: MyApp.Browser}
]
Supervisor.start_link(children, strategy: :one_for_one)
# Then use the named process
Bibbidi.Commands.BrowsingContext.get_tree(MyApp.Browser)
```
## Available Command Modules
| Module | Commands |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `Bibbidi.Commands.BrowsingContext` | navigate, getTree, create, close, captureScreenshot, print, reload, setViewport, handleUserPrompt, activate, traverseHistory, locateNodes |
| `Bibbidi.Commands.Script` | evaluate, callFunction, getRealms, disown, addPreloadScript, removePreloadScript |
| `Bibbidi.Commands.Session` | new, end, status, subscribe, unsubscribe |
| `Bibbidi.Session` | Higher-level session lifecycle (new, end_session, status, subscribe, unsubscribe) |
## Keyboard Input
`Bibbidi.Keys` maps human-friendly key names to the Unicode values expected by
BiDi `keyDown`/`keyUp` actions, so you don't need to memorize WebDriver codepoints:
```elixir
alias Bibbidi.Keys
alias Bibbidi.Commands.Input
# Build a key action sequence
actions = [
%{
type: "key",
id: "keyboard",
actions: [
%{type: "keyDown", value: Keys.key(:enter)},
%{type: "keyUp", value: Keys.key(:enter)}
]
}
]
Input.perform_actions(conn, context, actions)
```
Accepts atoms (`:enter`, `:arrow_up`, `:f1`), PascalCase strings (`"Enter"`,
`"ArrowUp"`), or single characters that pass through unchanged (`"a"`, `"1"`).
Each command also has a corresponding struct in `Bibbidi.Commands.<Module>.<Command>` (e.g. `Bibbidi.Commands.BrowsingContext.Navigate`) that implements the `Bibbidi.Encodable` protocol for use with `Connection.execute/2`. Every command struct exposes [Zoi](https://hex.pm/packages/zoi) schemas via `schema/0`, `opts_schema/0`, and `result_schema/0` for runtime validation and introspection.
## Types
All named BiDi protocol types have corresponding modules under `Bibbidi.Types.*` — generated from the W3C CDDL spec. Each type module exposes a `schema/0` function and a `@type t` for use in specs and docs:
```elixir
Bibbidi.Types.BrowsingContext # browsingContext.BrowsingContext (text alias)
Bibbidi.Types.Script.Target # script.Target (choice union)
Bibbidi.Types.Script.ContextTarget # script.ContextTarget (struct-like map)
Bibbidi.Types.Script.ResultOwnership # script.ResultOwnership (string enum)
```
Command struct moduledocs cross-reference these types with ExDoc links, so you can click through from a command's field documentation to the type definition.
## Workflow Builder
Bibbidi includes an Igniter generator that scaffolds a Multi-style pipeline
builder into your project:
```bash
mix bibbidi.gen.workflow
```
This generates an `Op` module for composing commands, an `Operation` record
for tracking execution, and a sequential `Runner` — all yours to own and modify.
```elixir
alias MyApp.Bibbidi.{Op, Runner}
alias Bibbidi.Commands.BrowsingContext
op =
Op.new()
|> Op.send(:nav, %BrowsingContext.Navigate{
context: ctx, url: "https://example.com", wait: "complete"
})
|> Op.send(:tree, %BrowsingContext.GetTree{})
{:ok, results, operation} = Runner.execute(conn, op)
```
See the [Op Workflow example](examples/op_workflow/op_workflow.md) for a standalone
Mix project demonstrating the pattern.
## Livebook
Try the [Interactive Browser](examples/interactive_browser.livemd) Livebook for a
GUI that lets you navigate, click, screenshot, run JavaScript, and view console
logs — all from your Browser!.
[](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fpetermueller%2Fbibbidi%2Fblob%2Fmain%2Fexamples%2Finteractive_browser.livemd)
## Architecture
- **`Bibbidi.Connection`** — GenServer owning the WebSocket. Correlates command IDs to callers, dispatches events to subscribers.
- **`Bibbidi.Protocol`** — Pure JSON encode/decode, no process state.
- **`Bibbidi.Transport`** — Behaviour for swappable WebSocket transports.
- **`Bibbidi.Transport.MintWS`** — Default transport using [mint_web_socket](https://hex.pm/packages/mint_web_socket).