ferridriver-test
Playwright-compatible test runner for Rust. Parallel workers, DAG-resolved fixtures, hooks, retries with flaky detection, auto-retrying assertions, text and pixel-diff snapshots, Playwright-compatible traces, and a wide set of reporters.
Quick start
use *;
async
async
Harness:
// tests/harness.rs
main!;
# Cargo.toml
[[]]
= "e2e"
= "tests/harness.rs"
= false
[]
= "0.2"
#[ferritest] attributes
retries = N # per-test retry count
timeout = "30s" # per-test timeout — duration string ("500ms", "30s", "5m")
tag = "smoke" # tag for --tag filtering (repeatable)
skip # unconditional skip
skip = "firefox" # conditional skip; condition is browser name, platform name,
# env var, "ci", or a "!" negation
skip = "firefox | flaky" # condition | reason
slow # 3x default timeout
slow = "ci" # conditional slow
fixme # known broken (skipped, but reported separately from skip)
fixme = "webkit" # conditional fixme
fail # expect failure: pass iff the body fails
fail = "linux" # conditional expect-failure
only # isolate one test (--forbid-only catches strays in CI)
info = "JIRA-123" # arbitrary metadata, attached to the test result
use_options = r#"{ ... }"# JSON overrides for launch / context options
Parameterized tests
)]
async
Generates one test per row, named title_check (<row values>).
Built-in fixtures
| Name | Scope | Type |
|---|---|---|
browser |
Worker | Arc<Browser> |
context |
Test | Arc<ContextRef> |
page |
Test | Arc<Page> |
test_info |
Test | Arc<TestInfo> |
Resolution walks Global → Worker → Test. Workers reuse the browser; each
test gets a fresh context and page. Custom fixtures register at the
FixturePool level with a scope tag; dependencies form a DAG validated at
startup.
Hooks
async // once per suite per worker
async // once per suite per worker (runs on failure)
async // before every test
async // after every test, even on failure
Expect matchers (38)
All matchers support .not, .with_timeout(Duration), .with_message(&str),
.soft(). Auto-retry on the Playwright polling schedule
[100, 250, 500, 1000, ...] ms up to expect_timeout (default 5000 ms).
Page (4): to_have_title, to_contain_title, to_have_url, to_contain_url.
Locator — visibility / state (10): to_be_visible, to_be_hidden,
to_be_enabled, to_be_disabled, to_be_checked, to_be_editable,
to_be_attached, to_be_empty, to_be_focused, to_be_in_viewport.
Locator — text / value (6): to_have_text, to_contain_text,
to_have_value, to_have_values, to_have_texts, to_contain_texts.
Locator — attributes (9): to_have_attribute, to_have_class,
to_contain_class, to_have_css, to_have_id, to_have_role,
to_have_accessible_name, to_have_accessible_description,
to_have_accessible_error_message.
Locator — other (5): to_have_js_property, to_have_count,
to_match_snapshot, to_have_screenshot, to_match_aria_snapshot.
Poll / satisfy (4): to_equal, to_satisfy, to_pass,
to_pass_with_options.
Reporters
Built-in reporter names (passed as [[test.reporter]] name = "..." or
--reporter NAME[:OPTIONS] on the CLI):
terminal, progress, dot, json, junit, html, blob, allure,
github, rerun, messages / ndjson (Cucumber Messages), usage,
cucumber-json, empty.
Multiple reporters can run simultaneously — events fan out via a broadcast bus.
CLI flags (after --)
--headed Show browser window
--backend NAME cdp-pipe | cdp-raw | webkit | bidi
--browser NAME chromium | firefox | webkit (selects default backend)
-j N, --workers N Parallel workers
--retries N Retry failed tests
--timeout MS Per-test timeout
--expect-timeout MS Assertion timeout
-g PATTERN, --grep ... Filter by test name (regex, case-insensitive)
--grep-invert PATTERN Exclude tests matching pattern
--tag NAME Filter by tag
--shard N/TOTAL Run only shard N of TOTAL
--list List tests without running
--update-snapshots Update snapshot files
--last-failed Re-run only previously failed tests
--forbid-only Fail if any #[ferritest(only)] is present
-c, --project NAME Filter by project
--reporter SPEC Reporter name (repeatable)
--profile NAME Apply a named [test.profiles.NAME] preset
Architecture
The TestRunner::run() pipeline is the single execution path for
#[ferritest], #[ferritest_each], BDD scenarios (via ferridriver-bdd),
and any external translator.
TestPlan
└── filter (shard, grep, tag, only, last-failed, forbid-only)
└── validate fixture DAG
└── run global setup
└── Dispatcher (MPMC work-stealing channel)
├── Worker 0 Browser
├── Worker 1 Browser
└── ... (concurrent launch via tokio::join!)
└── collect results (retry failed tests via re-enqueue)
└── run global teardown
└── exit code
Workers launch browsers concurrently (not serially) — overlapping launches save 80–100 ms per extra worker on warm machines.
License
MIT OR Apache-2.0