Bibbidi

Bibbidi icon

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"}
  ]
end

Quick Start

Launch a browser with BiDi support (Firefox has native support):

firefox --headless --remote-debugging-port=9222

Then 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
end

Function 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
end

Listening 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")
end

Function 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")
end

Supervision

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!.

Run in Livebook

Architecture