# 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.1.0"}
]
end
```
## Quick Start
Launch a browser with BiDi support (Firefox has native support):
```bash
firefox --headless --remote-debugging-port=9222
```
Then in IEx:
```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})
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; }",
%{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)
```
## Example Module
A copy-pasteable module showing common patterns:
```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})
# Extract all links
{:ok, %{"result" => %{"value" => links}}} =
Script.evaluate(
conn,
~s|Array.from(document.querySelectorAll("a"), a => a.href)|,
%{context: context}
)
%{title: title, links: links}
after
Session.end_session(conn)
Connection.close(conn)
end
end
end
```
## Listening for Events
```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
```
## 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) |
## 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).