# Wallaby runs each test case in its own browser, so two users can share a page

> Wallaby is a browser test library for Elixir web applications that manages browser processes itself, runs each ExUnit test case concurrently, and can hand several sessions to one test so two users interact with the same page. It suits Phoenix applications that want feature-level coverage without maintaining a Selenium grid, and it requires you to solve Ecto sandbox sharing yourself.

**elixir-wallaby/wallaby** — Concurrent browser tests for your Elixir web apps.

- Repository: https://github.com/elixir-wallaby/wallaby
- Website: https://twitter.com/elixir_wallaby
- Stars: 1,806 · Forks: 210
- Language: Elixir
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/elixir-wallaby-wallaby

## Tests are pipelines, not assertion lists

The API shape is the first thing to notice. Wallaby operations are functions that take a session and return a session, so a test reads as one continuous chain rather than as a sequence of independent calls. The README's example is a todo application:

```elixir
feature "users can create todos", %{session: session} do
  session
  |> visit("/todos")
  |> fill_in(text_field("New Todo"), with: "Write my first Wallaby test")
  |> click(button("Save"))
  |> assert_has(css(".alert", text: "You created a todo"))
end
```

Two details carry the design. Queries come from Wallaby.Query, imported selectively with `only:` so that text_field/1, button/1 and css/2 do not collide with your own functions. And assertions are part of the pipeline, so a failing assert_has leaves you the chain up to that point.

Queries match on visible text and CSS selectors rather than on internal attributes, which means a test survives a class rename but breaks when a user-visible label changes. That is the right bias for feature tests and the wrong one if you want to assert on structure.

The test case is an ordinary ExUnit case, so `use ExUnit.Case, async: true` is what makes it run alongside other cases. Concurrency is ExUnit's, and Wallaby supplies the browsers.

## Multiple sessions in one test is the feature nothing else gives you cheaply

Because Wallaby manages browser processes rather than borrowing one, a single test can hold several sessions at once. The README's second example models two users messaging each other, with the feature callback destructuring %{sessions: [user1, user2]} from a @sessions 2 module attribute.

```elixir
@sessions 2
feature "That users can send messages to each other", %{sessions: [user1, user2]} do
  user1 |> visit(@page) |> fill_in(@message_field, with: "Hello there!") |> click(@share_button)
  user2 |> visit(@page) |> fill_in(@message_field, with: "Hello yourself") |> click(@share_button)

  user1 |> assert_has(message("Hello yourself"))
end
```

Each user drives its own browser, and the assertions cross over: user1 must see what user2 sent. That is how you test presence, sharing, notification and any other behaviour that only appears when two sessions interact.

It also has a cost that is easy to miss. Two browser sessions against one application means two HTTP client processes, and with Ecto each of those processes would normally open its own database connection with its own transaction, which is exactly what the sandbox is designed to prevent. Wallaby leaves that wiring to you, which is the main setup work in the README and the main source of debugging time.

## Installation: a test-only dependency, a driver, and a started application

Requirements are narrow and worth checking first. Wallaby requires Elixir 1.17+ and OTP 26+. It also requires bash to be installed, and the README warns specifically that bash is widely available but does not come pre-installed on Alpine Linux. That one line decides whether your CI image works.

The dependency is test-only, which is the correct scope and also means it never ships in a release:

```elixir
def deps do
  [
    {:wallaby, "~> 0.30", runtime: false, only: :test}
  ]
end
```

Then pick a driver in config. Chrome is the default:

```elixir
# Chrome
config :wallaby, driver: Wallaby.Chrome # default

# Selenium
config :wallaby, driver: Wallaby.Selenium
```

Choosing the driver means installing the matching WebDriver binary yourself. For Chrome that is chromedriver. For Selenium it is selenium, plus geckodriver if you target Firefox or chromedriver if you target Chrome. The README is explicit that the drivers are your responsibility, so a CI image needs that binary installed before mix test will pass.

Finally, start the application in test_helper.exs:

```elixir
{:ok, _} = Application.ensure_all_started(:wallaby)
```

If you use Ecto, one more key is required, because Wallaby needs to know which application owns the repo:

```elixir
config :wallaby, otp_app: :your_app
```

For Phoenix, the endpoint must actually serve requests in tests, which the README shows as `server: true` in config/test.exs, and test_helper.exs needs a base URL so relative paths resolve:

```elixir
Application.put_env(:wallaby, :base_url, YourAppWeb.Endpoint.url)
```

## The Ecto sandbox plug is what makes concurrent feature tests possible

This is the most specific instruction in the README and the one people search for. If your Phoenix application uses Ecto with a database that supports sandbox mode, you add the sandbox plug to your endpoint, at the top, before any other plug:

```elixir
defmodule YourAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :your_app

  if Application.compile_env(:your_app, :sandbox, false) do
    plug Phoenix.Ecto.SQL.Sandbox
  end

  # ...

  socket("/live", Phoenix.LiveView.Socket,
    websocket: [connect_info: [:user_agent, session: @session_options]]
  )
```

Position matters and the README says so: the plug must come before any other plug, otherwise the browser request bypasses it.

The user_agent in connect_info matters just as much. The README states it is needed to allow the database and browser session to be wired up correctly, and the mechanism is that the user_agent is how the plug identifies which test process owns the incoming request, so it can share that process's connection. Without it the browser request gets a different, unsandboxed connection and tests interfere with each other.

Then enable the sandbox in config/test.exs:

```elixir
config :your_app, :sandbox, Ecto.Adapters.SQL.Sandbox
```

The described effect is that the connection is owned by the test process and shared to the process receiving the browser's HTTP requests, so both see the same data.

## Expectations and stubs need their own allow clause

If you use Mox, the plain Ecto sandbox is not enough, because expectations are not database state. The README's answer is a custom sandbox module that delegates the database work and adds the Mox call:

```elixir
defmodule YourApp.Sandbox do
  def allow(repo, owner_pid, child_pid) do
    # Delegate to the Ecto sandbox
    Ecto.Adapters.SQL.Sandbox.allow(repo, owner_pid, child_pid)

    # Add custom process-sharing configuration
    Mox.allow(MyMock, owner_pid, child_pid)
  end
end
```

Then point the test config at it:

```elixir
config :your_app, :sandbox, YourApp.Sandbox
```

The pattern generalises to any resource that is owned by a process rather than stored in a table, and the file location the README suggests, test/support/sandbox.ex, keeps it out of the release build.

## mix test does not rebuild assets, and that will cost you an afternoon

The README flags this plainly: assets are not recompiled when you run mix test, which can leave you failing tests against JavaScript or CSS you already changed. Two fixes are offered.

For esbuild, add the build to the test alias:

```elixir
defp aliases do
  [
    "test": [
      "esbuild default",
      "ecto.create --quiet",
      "ecto.migrate",
      "test",
    ]
  ]
end
```

For webpack, either run the watcher separately from the assets directory, which the README gives as webpack --mode development --watch, or add an alias that shells out:

```elixir
defp aliases, do: [
  "test": [
    "assets.compile --quiet",
    "ecto.create --quiet",
    "ecto.migrate",
    "test",
  ],
  "assets.compile": &compile_assets/1
]
```

Note the ordering: assets first, then ecto.create --quiet, then ecto.migrate, then the tests. A browser test against a database that was never created or migrated fails in a way that looks like an application bug.

## Where Wallaby is the wrong tool

Consider the boundaries before you commit.

If your application logic is testable without a browser, do not reach for Wallaby. These tests launch real browser processes, they need a real endpoint serving, and they are slower than a unit test by an order of magnitude. Wallaby is for the behaviour that only exists end to end.

If your CI image is Alpine, install bash first or nothing runs.

If you cannot wire the sandbox plug and the user_agent connect_info, concurrent feature tests will produce cross-test data leakage that looks like a product bug. Single-session tests avoid it; multi-session ones do not.

If your application does not use Ecto, the sandbox work disappears, but so does the guarantee that tests are isolated, and you are then relying on a shared database and a cleanup strategy the README does not cover.

If you need Safari or mobile browsers, the drivers named in the README are chromedriver, selenium, geckodriver and chromedriver. Nothing else is documented.

On maintenance and licence: the repository is MIT licensed with a LICENSE.md and a NOTICE.md at the root, changelog history in CHANGELOG.md, and the release tags v0.30.11, v0.30.12 and v0.31.0 landed between 2025-10-29 and 2026-06-27. The last push was on 2026-10-14. The mix.exs dependency example still says ~> 0.30 while the newest tag is v0.31.0, so check which version your build resolves before you assume the docs match the code.

## Conclusion

Adopt Wallaby if you have a Phoenix application whose important behaviour is only visible in a browser, and if you are willing to put a sandbox plug and a user_agent connect_info in your endpoint so the test process and the browser request process share a database connection. Do not adopt it as a drop-in on Alpine without installing bash, which the README calls out, or as a unit testing replacement, since it launches real browsers and real endpoints. Check first which driver you will run, because Chrome is the default and the Selenium path additionally needs geckodriver or chromedriver on PATH, and remember that mix test does not recompile assets, so wire an esbuild or assets.compile alias before you spend an afternoon debugging stale JavaScript.

## FAQ

### What does Wallaby require before it will run?

Elixir 1.17 or later and OTP 26 or later, plus bash installed. The README notes that bash does not come pre-installed on Alpine Linux. You also need the WebDriver binary for your chosen driver, chromedriver for Chrome or selenium with geckodriver or chromedriver for the Selenium driver.

### How do I add Wallaby to an Elixir project?

Add {:wallaby, "~> 0.30", runtime: false, only: :test} to the deps list in mix.exs, set the driver with config :wallaby, driver: Wallaby.Chrome or Wallaby.Selenium, and start it in test_helper.exs with {:ok, _} = Application.ensure_all_started(:wallaby).

### How does Wallaby support testing two users at the same time?

Because it manages browser processes itself, a feature test can receive several sessions, as in the README example that destructures %{sessions: [user1, user2]} from a @sessions 2 attribute and has each user visit, fill in and click independently before asserting that each sees the other's message.

### Why do my Wallaby tests fail when I change JavaScript or CSS?

Assets are not recompiled when you run mix test, so tests can run against stale assets. The README suggests adding esbuild default to the test alias, or running webpack --mode development --watch, or defining an assets.compile alias ahead of ecto.create and ecto.migrate in the test alias.

### Do I need Ecto sandbox configuration to use Wallaby with Phoenix?

Not to run tests, but for concurrent ones. The README says to add the Phoenix.Ecto.SQL.Sandbox plug at the top of your endpoint before any other plug, pass user_agent in the socket connect_info so the database and browser session are wired together, and set config :your_app, :sandbox, Ecto.Adapters.SQL.Sandbox in config/test.exs.

## Sources

- [elixir-wallaby/wallaby on GitHub](https://github.com/elixir-wallaby/wallaby)
- [License: MIT](https://github.com/elixir-wallaby/wallaby/blob/main/LICENSE)
- [Project website](https://twitter.com/elixir_wallaby)
- [README](https://github.com/elixir-wallaby/wallaby/blob/main/README.md)
- [Releases](https://github.com/elixir-wallaby/wallaby/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/elixir-wallaby-wallaby
