Bibbidi
BEAM Interface to Browsers with BiDi — a nod to the Fairy Godmother's spell in Disney's Cinderella.
Low-level Elixir implementation of the W3C WebDriver BiDi Protocol.
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
def deps do
[
{:bibbidi, "~> 0.3.0"}
]
endQuick Start
Launch a browser with BiDi support (Firefox has native support):
firefox --headless --remote-debugging-port=9222Then in IEx:
Struct API
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
# 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)Example Module
A copy-pasteable module showing common patterns:
Struct API
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
endFunction API
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
endListening for Events
Struct API
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")
endFunction API
{: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")
endSupervision
Bibbidi doesn't impose a process tree. Add connections to your own supervisor:
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:
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 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:
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:
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.
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 for a standalone Mix project demonstrating the pattern.
Livebook
Try the Interactive Browser Livebook for a GUI that lets you navigate, click, screenshot, run JavaScript, and view console logs — all from your Browser!.
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.