Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ config :wallabidi,

## Credits

Wallabidi is built on the foundation of [Wallaby](https://github.com/elixir-wallaby/wallaby), created by [Mitchell Hanberg](https://github.com/mhanberg) and [contributors](https://github.com/elixir-wallaby/wallaby/graphs/contributors). The Browser, Query, Element, Feature, and DSL APIs are theirs. Wallabidi adds the BiDi transport layer, new DX features, and removes the Selenium/HTTP legacy code.
Wallabidi is built on the foundation of [Wallaby](https://github.com/elixir-wallaby/wallaby), the work of its original author and [many contributors](https://github.com/elixir-wallaby/wallaby/graphs/contributors) over the years, and currently maintained by [Mitchell Hanberg](https://github.com/mhanberg). The Browser, Query, Element, Feature, and DSL APIs are theirs. Wallabidi adds the BiDi transport layer, new DX features, and removes the Selenium/HTTP legacy code.

Licensed under MIT, same as Wallaby.

Expand Down
9 changes: 7 additions & 2 deletions integration_test/cases/browser/event_capture_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,19 @@ defmodule Wallabidi.Integration.Browser.EventCaptureTest do
# Lightpanda CDP, while bubble-phase listeners on document do.

use Wallabidi.Integration.SessionCase, async: false
# Needs a JS-capable browser (execute_script + real DOM event flow).
# :headless runs it on Lightpanda + both Chrome drivers and excludes the
# in-process LiveView driver, which has no execute_script.
@moduletag :headless

@base Application.compile_env(:wallabidi, :live_app_url, "http://localhost:4321")

@phases ["root-capture", "root-bubble", "document-capture", "document-bubble"]

describe "DOM event propagation parity (vanilla page)" do
# No capability tag — DOM event flow is the spec; every JS-capable
# driver should pass this. Filing the failures here is the point.
# DOM event flow is the spec; every JS-capable driver should pass this
# (module is tagged :headless, so it runs on Lightpanda + both Chrome
# drivers — not the in-process LiveView driver).
test "Wallabidi.Browser.click fires all four phases", %{session: session} do
session = visit(session, @base <> "/event-capture")
assert_all_zero(session)
Expand Down
44 changes: 22 additions & 22 deletions lib/wallabidi/browser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -871,29 +871,29 @@ defmodule Wallabidi.Browser do
defp click_deferred(parent, query) do
session = get_session(parent)

cond do
is_nil(session) or is_nil(session.driver_spec) ->
# No spec (LV driver, or unusual session shape) → fall through
# to the normal click. There's no awaiting machinery to skip.
click_auto(parent, query)
# No spec (LV driver, or unusual session shape) → fall through to the
# normal click; there's no awaiting machinery to skip.
if is_nil(session) or is_nil(session.driver_spec) do
click_auto(parent, query)
else
orchestrator = Wallabidi.Remote.Driver.Orchestrator

case find_lazy(parent, query) do
%Element{} = element ->
case orchestrator.click_deferred(session.driver_spec, element) do
{:ok, pre_page_id} ->
%{session | pending_await: {:page_ready_after, pre_page_id}}

{:error, _} ->
# Click dispatch failed (e.g. transport issue). Don't stash a
# half-baked await — fall back to whatever surface error
# handling the assertion does.
session
end

true ->
case find_lazy(parent, query) do
%Element{} = element ->
case Wallabidi.Remote.Driver.Orchestrator.click_deferred(session.driver_spec, element) do
{:ok, pre_page_id} ->
%{session | pending_await: {:page_ready_after, pre_page_id}}

{:error, _} ->
# Click dispatch failed (e.g. transport issue). Don't
# stash a half-baked await — fall back to whatever
# surface error handling the assertion does.
session
end

other ->
other
end
other ->
other
end
end
end

Expand Down
12 changes: 5 additions & 7 deletions lib/wallabidi/installer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,11 @@ defmodule Wallabidi.Installer do
# cwd-relative for in-tree development (where Mix.Project is wallabidi
# itself).
defp bidi_server_dir do
case Application.app_dir(:wallabidi, "priv/bidi-server") do
path when is_binary(path) ->
if File.dir?(path), do: path, else: cwd_bidi_server_dir()

_ ->
cwd_bidi_server_dir()
end
# Application.app_dir/2 returns a path string (and raises ArgumentError
# if :wallabidi isn't loaded — caught below). Use it when it points at a
# real dir, otherwise fall back to cwd-relative for in-tree dev.
path = Application.app_dir(:wallabidi, "priv/bidi-server")
if File.dir?(path), do: path, else: cwd_bidi_server_dir()
rescue
ArgumentError -> cwd_bidi_server_dir()
end
Expand Down
6 changes: 5 additions & 1 deletion lib/wallabidi/live_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ defmodule Wallabidi.LiveView do
def set_latency(%Session{} = session, latency_ms)
when is_integer(latency_ms) and latency_ms >= 0 do
if remote?(session) do
_ = eval_silent(session, "window.liveSocket && window.liveSocket.enableLatencySim(#{latency_ms})")
_ =
eval_silent(
session,
"window.liveSocket && window.liveSocket.enableLatencySim(#{latency_ms})"
)
end

session
Expand Down
6 changes: 5 additions & 1 deletion lib/wallabidi/remote/chrome/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ defmodule Wallabidi.Remote.Chrome.Server do
calls_awaiting_readiness: []
]

@startup_timeout 15_000
# Time to wait for Chrome to emit its DevTools WebSocket URL. 30s (was
# 15s) — a cold Chrome on a contended CI runner can take >15s to start,
# which surfaced as intermittent :startup_timeout failures. Matches the
# chromium-bidi server's startup budget.
@startup_timeout 30_000

def child_spec(opts) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [opts]}}
Expand Down
46 changes: 34 additions & 12 deletions lib/wallabidi/remote/chrome/shared_connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@ defmodule Wallabidi.Remote.Chrome.SharedConnection do
# Lazy-connect on first `get/1` call — by then the driver supervisor
# has already started either a local Chrome (`Chrome.Server`) or we
# have a remote URL to connect to.
#
# A GenServer (not an Agent) serializes the connect: the first `get/1`
# establishes the one shared WS while concurrent callers park in the
# mailbox and are answered once it's ready. This means exactly one
# connection is ever created — no redundant sockets, no racing — and the
# one-time startup wait (Chrome booting, handled by `Chrome.Server`)
# isn't multiplied across callers. Once connected, `get/1` is a cheap
# `Process.alive?` reply.

use Agent
use GenServer

alias Wallabidi.Remote.WebSocket

# Must exceed Chrome.Server's @startup_timeout (30s): on a cold start the
# first get/1 blocks in connect until Chrome emits its DevTools URL, and
# parked callers wait for that same reply.
@get_timeout 40_000

def start_link(_opts) do
Agent.start_link(fn -> nil end, name: __MODULE__)
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end

@doc """
Expand All @@ -24,13 +37,22 @@ defmodule Wallabidi.Remote.Chrome.SharedConnection do
"""
@spec get(module) :: pid
def get(driver_mod) do
Agent.get_and_update(__MODULE__, fn
pid when is_pid(pid) ->
if Process.alive?(pid), do: {pid, pid}, else: connect(driver_mod)
GenServer.call(__MODULE__, {:get, driver_mod}, @get_timeout)
end

nil ->
connect(driver_mod)
end)
@impl true
def init(nil), do: {:ok, %{pid: nil}}

@impl true
def handle_call({:get, driver_mod}, _from, %{pid: pid} = state) do
if is_pid(pid) and Process.alive?(pid) do
{:reply, pid, state}
else
# Connect once. Concurrent callers are parked in the mailbox and get
# this same pid when we reply — no second connect, nothing to close.
new_pid = connect(driver_mod)
{:reply, new_pid, %{state | pid: new_pid}}
end
end

defp connect(driver_mod) do
Expand All @@ -55,11 +77,11 @@ defmodule Wallabidi.Remote.Chrome.SharedConnection do
end

# WebSocket.start_link would link to the *current caller* (the test
# process invoking Agent.get_and_update), so the shared WS would die
# when each test exits. Use `start/1` for an unlinked process whose
# lifetime is tied to the SharedConnection Agent instead.
# process invoking get/1), so the shared WS would die when each test
# exits. Use `start/1` for an unlinked process whose lifetime is tied
# to the SharedConnection Agent instead.
{:ok, pid} = WebSocket.start(ws_url)
{pid, pid}
pid
end

# Same /json/version discovery logic as ChromeCDP.SharedConnection.
Expand Down
24 changes: 20 additions & 4 deletions test/wallabidi/browser_paths_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,38 @@ defmodule Wallabidi.BrowserPathsTest do
# The lightpanda dep exposes target/0 + release/0, so this resolves.
assert is_binary(dir)
assert String.starts_with?(dir, Path.join(".browsers", "lightpanda"))
assert dir == Path.join([".browsers", "lightpanda", "#{Lightpanda.target()}-#{Lightpanda.release()}"])

assert dir ==
Path.join([
".browsers",
"lightpanda",
"#{Lightpanda.target()}-#{Lightpanda.release()}"
])
end
end

describe "chrome_for_testing_unsupported?/2" do
test "true only on arm/aarch64 Linux" do
assert BrowserPaths.chrome_for_testing_unsupported?({:unix, :linux}, "aarch64-unknown-linux-gnu")
assert BrowserPaths.chrome_for_testing_unsupported?({:unix, :linux}, "armv7l-unknown-linux-gnueabihf")
assert BrowserPaths.chrome_for_testing_unsupported?(
{:unix, :linux},
"aarch64-unknown-linux-gnu"
)

assert BrowserPaths.chrome_for_testing_unsupported?(
{:unix, :linux},
"armv7l-unknown-linux-gnueabihf"
)
end

test "false on x86_64 Linux (Chrome for Testing ships a build)" do
refute BrowserPaths.chrome_for_testing_unsupported?({:unix, :linux}, "x86_64-pc-linux-gnu")
end

test "false on macOS regardless of arch (system Chrome / CfT both fine)" do
refute BrowserPaths.chrome_for_testing_unsupported?({:unix, :darwin}, "aarch64-apple-darwin")
refute BrowserPaths.chrome_for_testing_unsupported?(
{:unix, :darwin},
"aarch64-apple-darwin"
)
end

test "false on Windows" do
Expand Down
Loading