ferridriver-bdd 0.3.0

BDD/Cucumber test framework for ferridriver. 144 built-in Gherkin steps backed by the Page API.
Documentation

ferridriver-bdd

crates.io docs.rs License

Cucumber / Gherkin BDD framework for ferridriver. Translates .feature files into the same TestPlan the #[ferritest] runner executes — same parallel workers, retries, fixtures, reporters. Step bodies are written in Rust or in JavaScript / TypeScript; both share one registry.

Rust step bodies

use ferridriver_bdd::prelude::*;

#[given("I navigate to {string}")]
async fn navigate(world: &mut BrowserWorld, url: String) {
    world.page().goto(&url, None).await.unwrap();
}

#[when("I click {string}")]
async fn click(world: &mut BrowserWorld, selector: String) {
    world.page().locator(&selector).click().await.unwrap();
}

#[then("the page should contain text {string}")]
async fn check_text(
    world: &mut BrowserWorld,
    text: String,
) -> Result<(), StepError> {
    let body = world
        .page()
        .locator("body")
        .text_content()
        .await
        .map_err(|e| step_err!("{e}"))?
        .unwrap_or_default();
    if !body.contains(&text) {
        return Err(step_err!("text {text:?} not found"));
    }
    Ok(())
}

Wire a binary entry point:

// tests/bdd.rs
ferridriver_bdd::bdd_main!();
[[test]]
name = "bdd"
path = "tests/bdd.rs"
harness = false

[dev-dependencies]
ferridriver-bdd = "0.2"
ferridriver-test = "0.2"
cargo test --test bdd
# or
ferridriver bdd tests/features/

JavaScript / TypeScript step bodies

// steps/login.ts
Given('I navigate to {string}', async function (url: string) {
  await this.page.goto(url);
});

When('I click {string}', async function (selector: string) {
  await this.page.locator(selector).click();
});

Then('the URL contains {string}', async function (fragment: string) {
  if (!this.page.url().includes(fragment)) throw new Error('mismatch');
});
ferridriver bdd --steps 'steps/**/*.{js,ts}' tests/features/

Given / When / Then / Step / Before / After / BeforeAll / AfterAll / BeforeStep / AfterStep / defineParameterType / setWorldConstructor / setDefaultTimeout / setDefinitionFunctionWrapper are globals. this is the World — page, context, browser, request, parameters, attach, log, skip.

Files are bundled with rolldown (TypeScript + node_modules + tree-shake), compiled to QuickJS bytecode once at startup, and Module::loaded per worker. The bytecode cache is content-hashed and in-memory. No Node, no Bun in the run path.

Macros

#[given(EXPR)]           #[when(EXPR)]              #[then(EXPR)]
#[step(EXPR)]            #[given(regex = PATTERN)]  // …same for when/then
#[before(scenario)]      #[after(scenario)]
#[before(scenario, tags = "@auth", order = 10)]
#[before(all)]           #[after(all)]
#[before(feature)]       #[before(step)]            // and matching afters
#[param_type(name = "color", regex = "red|green|blue")]

Parameter extraction is type-directed:

  • String{string}
  • i64{int}
  • f64{float}
  • Custom {name} → registered regex (extract as String)

table: &DataTable / data_table: &DataTable and docstring: &str / doc_string: &str are recognised by name and pulled in after positional parameters.

Hooks

Eight hook points, both Rust and JS:

BeforeAll, AfterAll, BeforeFeature, AfterFeature, BeforeScenario, AfterScenario, BeforeStep, AfterStep. Tag filters and explicit order are supported on every point. After* hooks run even on failure (cleanup guarantee).

Gherkin coverage

Full Gherkin 6+: Features, Rules, Backgrounds, Scenarios, Scenario Outlines (with named Examples blocks), tags (boolean expressions: and, or, not, parens), data tables (with .hashes(), .rows_hash(), .transpose(), .as_type::<T>()), doc strings (with media-type hints like """json), the asterisk (*) keyword, and i18n keywords via --language or # language: xx.

Scenario Outline placeholders (<key>) substitute into step text, table cells, and doc strings recursively. At runtime, $key interpolation reaches into world.vars() / world.set_var(name, value).

Built-in steps (144)

Source: crates/ferridriver-bdd/src/steps/. Counts reflect actual #[given] / #[when] / #[then] / #[step] registrations.

Module Count Coverage
assertion 34 Text, visibility, value, attribute, class, state, count, role, ARIA
interaction 20 Click / double-click / right-click, fill, clear, type, hover, focus, blur, drag, scroll, select, check, uncheck
network 14 Route, fulfill, continue, abort, request / response waits, HAR
api 11 API request context: GET/POST/PUT/DELETE/PATCH, headers, body, status / JSON assertions
storage 8 localStorage / sessionStorage get / set / clear / remove
wait 7 Wait for selector / text / navigation / seconds / load state
navigation 6 Navigate, back, forward, reload, URL assertions
frame 6 Switch frames by name / index, main frame, frame queries
dialog 5 Accept / dismiss, prompt text, assert message
emulation 5 Viewport, user agent, geolocation, color scheme, network
mouse 5 Move to coordinates, scroll by delta, wheel, button holds
window 5 Window size, maximize / minimize, tab / window switching
keyboard 4 Press key, press on selector, repeat N times, type slowly
javascript 3 Execute, evaluate, inject script
cookie 3 Add, delete, clear all
screenshot 3 Full page, named file, element-scoped
variable 3 Set, store text / attribute / property / count of selector
file 2 Upload to input, assert download

Call StepRegistry::reference() from a bdd_main!() binary for the live expression list with parameter types.

Reporters

Same reporter family as ferridriver-test plus BDD-specific renderers:

terminal (Feature → Scenario → Step hierarchy with colours), json, junit, html, cucumber-json, messages / ndjson (Cucumber Messages NDJSON), usage, rerun, progress, dot.

Public API (programmatic use)

Bypass the CLI / macros and drive the executor directly when embedding:

use ferridriver_bdd::{registry::StepRegistry, executor::ScenarioExecutor};
use std::sync::Arc;
use std::time::Duration;

let registry = StepRegistry::build();
let executor = ScenarioExecutor::new(
    Arc::new(registry),
    Duration::from_millis(5000),
    /* strict */ false,
    /* screenshot_on_failure */ true,
);
let result = executor
    .run_scenario_observed(&mut world, &scenario, &observer)
    .await;

StepRegistry::register() / register_regex() accept handler closures — useful when registering steps from a host other than the macros (an MCP plugin, an external test driver).

License

MIT OR Apache-2.0