# PhoenixHtmldriver
A lightweight Phoenix library for testing pure HTML interactions without the overhead of a headless browser. PhoenixHtmldriver provides a human-like API for testing Phoenix applications' HTML output, inspired by Capybara and Wallaby but optimized for pure HTML testing.
## Features
- **Session-based API**: Chain interactions naturally with a session object
- **Lightweight**: No headless browser overhead - pure HTML parsing with Floki
- **Phoenix Integration**: Seamlessly works with Phoenix.ConnTest
- **Human-readable**: Intuitive API that mirrors user interactions
- **Fast**: Significantly faster than browser-based testing
## Installation
Add `phoenix_htmldriver` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:phoenix_htmldriver, "~> 0.4.0"}
]
end
```
## Usage
### Basic Example with `use PhoenixHtmldriver` (Recommended)
The easiest way to use PhoenixHtmldriver is with the `use PhoenixHtmldriver` macro, which automatically configures the endpoint:
```elixir
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
use PhoenixHtmldriver # Automatically configures endpoint!
test "login flow", %{conn: conn} do
# No manual setup needed - conn is automatically configured
visit(conn, "/login")
|> fill_form("#login-form", username: "alice", password: "secret")
|> submit_form("#login-form")
|> assert_text("Welcome, alice")
|> assert_selector(".alert-success")
end
end
```
**Important:** Make sure to set `@endpoint` **before** `use PhoenixHtmldriver`:
```elixir
defmodule MyTest do
use ExUnit.Case
@endpoint MyAppWeb.Endpoint # Must come before use PhoenixHtmldriver
use PhoenixHtmldriver
# Tests...
end
```
### Manual Configuration (Advanced)
If you need more control, you can import functions directly:
```elixir
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
import PhoenixHtmldriver
setup %{conn: conn} do
conn = Plug.Conn.put_private(conn, :phoenix_endpoint, MyAppWeb.Endpoint)
%{conn: conn}
end
test "login flow", %{conn: conn} do
session = visit(conn, "/login")
# ...
end
end
```
### Navigation
```elixir
# Visit a page
session = visit(conn, "/home")
# Click a link by selector
session = click_link(session, "#about-link")
# Click a link by text
session = click_link(session, "About Us")
# Get current path
path = current_path(session)
```
### Forms
```elixir
# Fill in a form (prepares values)
session = fill_form(session, "#contact-form",
name: "Alice",
email: "alice@example.com",
message: "Hello!"
)
# Submit a form
session = submit_form(session, "#contact-form")
# Or submit with values directly
session = submit_form(session, "#contact-form",
name: "Alice",
email: "alice@example.com"
)
# CSRF tokens are automatically extracted and included!
# No manual CSRF handling needed for forms with CSRF protection
```
### Assertions
```elixir
# Assert text is present
session = assert_text(session, "Welcome back")
# Assert element exists
session = assert_selector(session, ".alert-success")
session = assert_selector(session, "#user-profile")
# Assert element does not exist
session = refute_selector(session, ".alert-danger")
```
### Finding Elements
```elixir
# Find a single element
{:ok, element} = find(session, ".user-name")
text = PhoenixHtmldriver.Element.text(element)
# Find all matching elements
elements = find_all(session, ".list-item")
length(elements) # => 5
# Get element attributes
{:ok, element} = find(session, "#profile-link")
href = PhoenixHtmldriver.Element.attr(element, "href")
has_id = PhoenixHtmldriver.Element.has_attr?(element, "id")
```
### Inspecting Responses
```elixir
# Get current HTML
html = current_html(session)
# Get current path
path = current_path(session)
```
## API Reference
### Session Functions
- `visit(conn, path)` - Navigate to a path and create a new session
- `click_link(session, selector_or_text)` - Click a link by selector or text
- `fill_form(session, selector, values)` - Fill in form fields
- `submit_form(session, selector, values \\ [])` - Submit a form
- `assert_text(session, text)` - Assert text is present
- `assert_selector(session, selector)` - Assert element exists
- `refute_selector(session, selector)` - Assert element doesn't exist
- `find(session, selector)` - Find a single element
- `find_all(session, selector)` - Find all matching elements
- `current_path(session)` - Get current request path
- `current_html(session)` - Get current response HTML
### Element Functions
- `PhoenixHtmldriver.Element.text(element)` - Get element text content
- `PhoenixHtmldriver.Element.attr(element, name)` - Get attribute value
- `PhoenixHtmldriver.Element.has_attr?(element, name)` - Check if attribute exists
## Session and Cookie Handling
PhoenixHtmldriver automatically preserves session cookies across requests, enabling you to test multi-step flows naturally:
```elixir
test "login flow with session", %{conn: conn} do
visit(conn, "/login")
|> submit_form("#login-form", email: "user@example.com", password: "secret")
|> assert_text("Welcome back!")
|> click_link("Profile")
|> assert_text("user@example.com")
# Session cookies are automatically preserved throughout!
end
```
**How it works:**
- Session cookies from responses are automatically extracted
- Subsequent requests (`visit`, `click_link`, `submit_form`) include these cookies
- This enables proper session-based authentication and CSRF validation
## CSRF Protection
PhoenixHtmldriver automatically handles CSRF tokens for you! When submitting forms, it:
1. Looks for a hidden `_csrf_token` input field within the form
2. Falls back to a `<meta name="csrf-token">` tag in the document head
3. Automatically includes the token in POST, PUT, PATCH, and DELETE requests
4. Never overrides tokens you explicitly provide
5. **Works seamlessly with session cookies** to ensure tokens validate correctly
This means you can test forms with CSRF protection without any extra setup:
```elixir
test "login with CSRF protection", %{conn: conn} do
visit(conn, "/login")
|> submit_form("#login-form", email: "user@example.com", password: "secret")
|> assert_text("Welcome back!")
# Both CSRF token AND session cookie were automatically handled!
end
```
## How It Works
PhoenixHtmldriver uses Floki to parse HTML and Plug.Test to simulate HTTP requests. Unlike browser-based testing tools, it works directly with your Phoenix application's conn struct, making tests fast and reliable.
The library maintains a Session struct that tracks:
- The current conn
- The parsed HTML document
- The latest response
- The endpoint being tested
This allows for natural chaining of interactions while maintaining the state of the "browsing session".
## Comparison with Other Tools
### vs. Wallaby/Hound (Browser-based)
- **Faster**: No browser startup overhead
- **Simpler**: No JavaScript support, pure HTML only
- **More Reliable**: No flaky browser interactions
- **Limited**: Cannot test JavaScript behavior
### vs. Phoenix.ConnTest (Direct)
- **More Natural**: Human-like API vs. low-level HTTP
- **Chainable**: Session-based interactions
- **HTML-aware**: Built-in selectors and assertions
- **Simpler Forms**: Easy form filling and submission
## When to Use
PhoenixHtmldriver is perfect for:
- Testing server-rendered HTML applications
- Controller and view testing
- Form submission flows
- Multi-step interactions
- Fast integration tests
It's not suitable for:
- Testing JavaScript-heavy applications
- Testing client-side interactions
- Testing WebSocket behavior
## Examples
### Testing a Multi-Step Flow
```elixir
test "user registration and profile update", %{conn: conn} do
# Register a new user
session =
visit(conn, "/register")
|> fill_form("#registration-form",
username: "alice",
email: "alice@example.com",
password: "secret123"
)
|> submit_form("#registration-form")
# Verify registration success
session = assert_text(session, "Welcome, alice!")
# Navigate to profile
session = click_link(session, "Edit Profile")
# Update profile
session
|> fill_form("#profile-form", bio: "Hello, I'm Alice")
|> submit_form("#profile-form")
|> assert_text("Profile updated successfully")
end
```
### Testing with Assertions
```elixir
test "validates form submission", %{conn: conn} do
visit(conn, "/contact")
|> submit_form("#contact-form", name: "") # Submit empty form
|> assert_text("Name is required")
|> assert_selector(".error-message")
|> refute_selector(".success-message")
end
```
## Setting Up in Your Tests
### Option 1: Using `use PhoenixHtmldriver` (Recommended)
The simplest way - just add `use PhoenixHtmldriver` after setting `@endpoint`:
```elixir
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
@endpoint MyAppWeb.Endpoint # Must come first!
use PhoenixHtmldriver
# No setup needed - conn is automatically configured!
test "home page", %{conn: conn} do
visit(conn, "/")
|> assert_text("Welcome")
end
end
```
### Option 2: Manual Setup
If you need more control or prefer explicit configuration:
```elixir
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
import PhoenixHtmldriver
setup %{conn: conn} do
conn = Plug.Conn.put_private(conn, :phoenix_endpoint, MyAppWeb.Endpoint)
%{conn: conn}
end
test "home page", %{conn: conn} do
session = visit(conn, "/")
assert_text(session, "Welcome")
end
end
```
## License
MIT
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.