BDD JavaScript / TypeScript API
Cucumber-js-shaped surface, native-backed. Step bodies in JS / TS run
through the same Rust TestRunner that drives Rust #[ferritest]
tests — same workers, retries, reporters, fixtures.
Step registration
Pattern can be a Cucumber expression (default) or a RegExp:
Per-step timeout
Per-step options object goes between pattern and handler:
Binding steps to a fixture chain
bindSteps(test) returns the same registrars bound to a test object's
fixture chain — the native primitive behind playwright-bdd's
createBdd(test). A step registered through it resolves its first
parameter from that chain rather than from the ambient world:
bindSteps(mergeTests(a, b)) binds to both chains at once. Passing
anything that is not a test object throws, naming what was expected.
What a step receives
One object per scenario is both this and the first argument of every
step and hook in it:
- the scenario's browser bindings —
page,context,request,browser— plusbrowserName,headless,isMobile,hasTouch,baseURLandtestInfo; - the cucumber World surface:
parameters,attach,log,skip; - every fixture the scenario's steps and hooks named, resolved from the
chain they were bound to, plus every
autofixture of that chain; - when the suite called
setWorldConstructor, that instance as the object's prototype, so its methods resolve throughthiswhile a step's ownthis.foo = …lands on the scenario object and is still there in the next step.
Fixtures are set up in dependency order before the first Before hook
and torn down in LIFO order after the last step and every After hook —
the same graph, the same use() handshake and the same rules a
Playwright spec gets, including { scope: 'worker' } fixtures shared by
every scenario one worker runs, and option: true fixtures overridden
from the use block or an @use(...) tag.
A body whose first parameter is a plain identifier (the classic
function (world) style) keeps working and still receives that same
object — it simply names no fixtures, so none of the chain's are set up
for it. That answer is the named diagnostic
bdd.fixtures.not_destructured, visible under --verbose or
RUST_LOG=ferridriver::script=debug.
Steps in one scenario must resolve from one chain. Bound to unrelated
test objects, the scenario fails before its first step, naming both
fixture sets and pointing at mergeTests(...) — nothing could hand a
scenario two disjoint bags at once.
BeforeAll / AfterAll run with no scenario, so they receive the
world-shaped object (parameters, attach, log, skip, and the
setWorldConstructor instance as prototype) rather than a fixture bag.
Hooks
Tag expressions support the full boolean grammar: @smoke and not @wip,
(@fast or @critical) and not @wip.
After* hooks run even when earlier hooks or steps failed (cleanup
guarantee). Hook order: Before hooks run in ascending order, After
hooks run in descending — cleanup mirrors setup.
The World
this inside any step or hook is the World — a per-scenario object
carrying fixtures and helpers:
Custom World
setWorldConstructor is per-VM (last call wins). Fixtures (page,
context, browser, request) are augmented onto the instance after
construction.
Step return values
Parameter types
Built-in Cucumber expression parameters:
Custom parameter type
regexp takes a string or a RegExp; its pattern keeps its own regex
semantics inside the expression, so alternations and character classes
match as written. Built-in names (string, int, float, word, {})
cannot be redefined, and registering the same name twice is an error —
both are refused at load with a message naming the type.
Type inference: Given('I have {int} {string}', (count, item) => {})
gives count: number, item: string in TS-aware editors.
Data tables
A step that ends with a table receives a DataTable argument by name:
DataTable methods
Doc strings
A """ block after a step is passed as a string argument:
Media-type hints ("""json, """yaml) are parsed and surfaced in the
report but do not change the value type (always string).
Defaults and globals
setParallelCanAssign is accepted for cucumber-js compat but is inert:
ferridriver parallelises at the test-runner worker level (one VM per
worker), not cucumber-js's per-pickle scheduler.
Built-in Rust step library
There is a shipped Rust step library (ferridriver-bdd/src/steps/)
of 144 steps — see Built-in steps. Those are registered
via #[given] / #[when] macros on the Rust side and merge into the
same registry as your JS / TS steps. A .feature file can mix steps
from both languages freely.
Imports
No bare specifier resolution from inside extensions / steps outside
what rolldown can resolve at bundle time. There is no runtime
require() and no Node module loader.
See also
- BDD overview — Rust + JS, hybrid suites
- BDD running — CLI, reporters, profiles
- Extensions — sharing a file between MCP + BDD
- Sandbox —
process,fetch,fs, what is absent