npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

effect-bdd

v0.4.0

Published

An Effect-native API for testing Gherkin feature files

Readme

effect-bdd

An Effect-native runner for testing Gherkin feature files with explicit, typed scenario chains.

effect-bdd uses Cucumber's parser/compiler for Gherkin syntax, but not Cucumber's mutable World model. Your code declares the executable scenario chains; the .feature file is verified against those chains position by position.

This package currently tracks the Effect v4 beta release train. Use matching 4.0.0-beta.x versions of effect and Effect platform packages.

Install

pnpm add effect-bdd [email protected]

Quick Start

import { Bdd } from "effect-bdd";
import { Effect, Schema } from "effect";

const expected = Bdd.capture("expected", Schema.FiniteFromString);

const givenNoCounter = Bdd.given`no counter exists`(() => Effect.void);
const whenCounterIsCreated = Bdd.when`the counter is created`(() => Effect.succeed(0));
const thenCounterValueIs = Bdd.then`the counter value is ${expected}`(
  ({ expected }: { readonly expected: number }, state: number) =>
    state === expected
      ? Effect.succeed(state)
      : Effect.fail(`expected ${expected}, got ${state}` as const),
);

const creatingACounter = Bdd.scenario("Creating a counter").pipe(
  givenNoCounter,
  whenCounterIsCreated,
  thenCounterValueIs,
);

const counter = Bdd.feature("Counter").pipe(creatingACounter);

const program = Bdd.run(
  counter,
  `
Feature: Counter

  Scenario: Creating a counter
    Given no counter exists
    When the counter is created
    Then the counter value is 0
`,
).pipe(Effect.provide(Bdd.layerCucumber));

The Model

A feature is made of explicit scenario chains:

  • Bdd.feature(title) creates a feature definition.
  • Bdd.scenario(title) creates a pipeable scenario chain.
  • Bdd.given, Bdd.when, Bdd.then, and Bdd.step create reusable step values.
  • Steps pipe into scenarios; scenarios pipe into features.
  • Each step returns an Effect containing the next state.
  • State may evolve across a scenario: void -> Draft -> Result -> Asserted.
  • There is no feature-level initial state. The first step sets up the first useful state.

Feature and scenario definitions expose their Gherkin labels as title.

The runner parses the feature source, compiles it with Cucumber, pairs each source scenario with the Bdd.scenario(...) chain of the same title, verifies every step in order, then runs the chain.

Upgrading to 0.4.0

Version 0.4.0 makes BDD-owned labels consistently use title:

  • BDD label properties are now feature.title and scenario.title; do not use JavaScript Function.name as a scenario label.
  • Bdd.Report.scenarios[number].name is now Bdd.Report.scenarios[number].title.
  • The CLI title filter is now --title; --name was removed. The short alias remains -n.
  • JSON diagnostics now use featureTitle / scenarioTitle instead of featureName / scenarioName.

Step Timeouts

Steps are unbounded by default. Configure a run-level timeout when a stuck promise, socket, browser, or polling loop should fail the scenario instead of hanging the run:

import { Bdd } from "effect-bdd";
import { Duration, Effect } from "effect";

declare const counter: Bdd.Feature;
declare const source: string;

const program = Bdd.run(counter, source, {
  stepTimeout: Duration.seconds(5),
}).pipe(Effect.provide(Bdd.layerCucumber));

Override the run-level timeout for a single slow step with Bdd.withTimeout:

import { Bdd } from "effect-bdd";
import { Duration, Effect } from "effect";

const thenProjectionCatchesUp = Bdd.then`the projection catches up`(() => Effect.void).pipe(
  Bdd.withTimeout(Duration.seconds(30)),
);

Timeouts are represented as StepError failures with the scenario, step text, and source line. The StepError.cause is a StepTimeoutError containing the configured Duration. Effect timeouts interrupt fibers, but synchronous infinite loops or non-interruptible native work can still block the process.

Recommended steps.ts Shape

Keep step modules boring:

imports
schemas and captures
domain types
pure helpers
reusable given/when/then step values
scenario chains
one exported Bdd.feature(...)

One Gherkin Feature: should map to one exported Bdd.feature("Feature name"). Do not split one feature across multiple exported feature definitions. Reusable steps can live anywhere, but compose them into a single feature export per feature name. When the CLI loads shared step modules, every exported Bdd.feature(...) is considered during discovery.

Backgrounds

Backgrounds are explicit leading steps in the chain.

Feature: Cart

  Background:
    Given an empty cart

  Rule: Taxed checkout
    Background:
      Given tax is enabled

    Scenario: Adding taxed items
      When 2 book are added
      Then the taxed total is 44

Cucumber compiles that scenario into this flat list:

Given an empty cart
Given tax is enabled
When 2 book are added
Then the taxed total is 44

So the chain must list the same steps:

Bdd.scenario("Adding taxed items").pipe(
  givenEmptyCart,
  givenTaxEnabled,
  whenBooksAdded,
  thenTaxedTotal
)

There is intentionally no Bdd.background(...) helper.

Drift Detection

The scenario chain must mirror the compiled feature-file steps exactly.

  • Missing background step: Scenario "X" has 4 source step(s), but its chain has 3 step(s).
  • Wrong keyword: Step 2 keyword mismatch: source is When, chain expects Given.
  • Wrong order or text: Step 2 text mismatch: source says "...", chain expects "...".
  • Missing scenario chain: Scenario has no matching Bdd.scenario chain.
  • Extra scenario chain: Scenario chain exported but no source scenario matched.
  • Extra feature export: Feature definition exported but no feature file matched.
  • Duplicate feature exports: CLI discovery fails with Multiple feature definitions matched "X".
  • Duplicate scenario chains in one feature: CLI discovery fails before running scenarios.

And and But inherit the previous concrete keyword before verification. Bdd.step is the escape hatch for phrases that are genuinely valid in any keyword position; use it sparingly.

Captures

Captures are named values inside a tagged-template step expression. The source text is a string; the capture's Schema decodes it before the step implementation runs.

import { Bdd } from "effect-bdd";
import { Effect, Schema } from "effect";

const expected = Bdd.capture("expected", Schema.FiniteFromString);

const thenTotalIs = Bdd.then`the cart total is ${expected}`(
  ({ expected }: { readonly expected: number }, state: { readonly total: number }) =>
    state.total === expected
      ? Effect.succeed(state)
      : Effect.fail(`expected ${expected}, got ${state.total}` as const),
);

Prefer strict schemas. Schema.FiniteFromString rejects "abc", "", and "Infinity" as MatchErrors.

Note: for now, examples annotate captured handler parameters explicitly. This keeps strict TypeScript and example checking honest around overloaded tagged-template inference.

DataTables and DocStrings

Use Bdd.table(schema) for Gherkin DataTables. The first row is headers; each later row is decoded by the row schema.

import { Bdd } from "effect-bdd";
import { Effect, Schema } from "effect";

const Item = Schema.Struct({
  sku: Schema.String,
  qty: Schema.FiniteFromString,
});

const whenItemsAreAdded = Bdd.when`the following items are added:`(
  Bdd.table(Item),
  (items: ReadonlyArray<typeof Item.Type>, state: ReadonlyArray<typeof Item.Type>) =>
    Effect.succeed([...state, ...items]),
);

Use Bdd.docString(schema) for larger step arguments, including JSON payloads.

import { Bdd } from "effect-bdd";
import { Effect, Option, Schema } from "effect";

const Payload = Schema.Struct({
  sku: Schema.String,
  qty: Schema.Number,
});

const whenRequestBodyIs = Bdd.when`the request body is:`(
  Bdd.docString(Schema.fromJsonString(Payload)),
  (payload: typeof Payload.Type) => Effect.succeed(Option.some(payload)),
);

Schema decode failures are preserved on MatchError.cause.

Services

Step implementations return normal Effect values, so they can require services in R and fail with typed errors in E.

import { Bdd } from "effect-bdd";
import { Context, Effect, Schema } from "effect";

class TaxRate extends Context.Service<
  TaxRate,
  {
    readonly rate: number;
  }
>()("TaxRate") {}

const expected = Bdd.capture("expected", Schema.FiniteFromString);

const thenTaxedTotalIs = Bdd.then`the taxed total is ${expected}`(
  ({ expected }: { readonly expected: number }, subtotal: number) =>
    Effect.gen(function* () {
      const taxRate = yield* TaxRate;
      const actual = Math.round(subtotal * (1 + taxRate.rate));
      return actual === expected
        ? subtotal
        : yield* Effect.fail(`expected ${expected}, got ${actual}` as const);
    }),
);

CLI

effect-bdd \
  --features "features/**/*.feature" \
  --steps "features/**/*.step.ts" \
  --reporter text

Important flags:

  • --features, -f: required, repeatable, supports *, ?, and **.
  • --steps, -s: required, repeatable.
  • --reporter, -r: repeatable; text, html, json, or junit.
  • --output-file.<reporter>: write a reporter to a file.
  • --tags: Cucumber-style tag expression with and, or, not, and parentheses.
  • --title, -n: run scenarios whose Feature / Scenario title contains the text.
  • --parallel: run scenarios concurrently.
  • --fail-fast: stop after the first failed scenario.
  • --step-timeout: maximum duration for each step, using Effect Duration input such as "500 millis" or "5 seconds".
  • --strict: fail the CLI on unused feature or scenario definitions. Without --strict, only unmatched selected feature files/scenarios fail the run.
  • --verbose: show passing scenarios in text output.

Glob Syntax

--features and --steps use effect-bdd's built-in glob resolver, not your shell's full glob language. Supported tokens are:

  • *: zero or more characters inside one path segment.
  • ?: exactly one character inside one path segment.
  • **: zero or more path segments.

Patterns without wildcards are treated as literal file paths. Brace expansion ({unit,e2e}), extglob, and shell character classes are not supported. Pass multiple --features or --steps flags instead; matches are unioned, deduped, and sorted.

Quote glob arguments in the shell so effect-bdd receives the pattern:

effect-bdd --features "features/**/*.feature" --steps "features/**/*.step.ts"

Focused Runs and Shared Steps

Focused runs are common in larger repos:

effect-bdd \
  --features "features/counter.feature" \
  --steps "features/**/*.step.ts"

That can load step modules for features you did not select. The selected .feature scenarios still must have matching Bdd.scenario(...) chains, but other loaded feature exports may be reported under Unused definitions: because their .feature files were outside this run.

Unused definitions are non-fatal by default, which keeps focused local runs useful. For full-suite CI, keep broad --features and --steps globs and add --strict so drift detection catches missing or extra definitions.

Node requires an explicit TypeScript loader for .ts step modules:

NODE_OPTIONS="--import tsx" pnpm exec effect-bdd \
  --features "features/**/*.feature" \
  --steps "features/**/*.step.ts"

Bun can load .ts step modules directly:

bunx --bun effect-bdd --features "features/**/*.feature" --steps "features/**/*.step.ts"

Supported Gherkin

Feature files are parsed and compiled with Cucumber's Gherkin implementation. The runner supports:

  • Feature
  • Scenario
  • Scenario Outline and Examples
  • Background
  • Rule
  • tags on features, rules, scenarios, and examples
  • Given, When, Then, And, and But
  • DataTables
  • DocStrings
  • comments and descriptions

Scenario Outlines are expanded before execution. Every Examples row runs the same source scenario chain independently.

Errors

Bdd.run fails with:

  • ParseError: invalid Gherkin.
  • MatchError: feature/scenario/step verification or argument decoding failed.
  • StepError: a matched step implementation failed or exceeded its configured timeout. Timed-out steps use StepTimeoutError as the StepError.cause.

Public API

Most users should import from effect-bdd and use the Bdd namespace:

  • constructors: Bdd.capture, Bdd.table, Bdd.docString, Bdd.feature, Bdd.scenario
  • steps: Bdd.given, Bdd.when, Bdd.then, Bdd.step
  • step metadata: Bdd.withTimeout
  • runner: Bdd.run
  • compiler service: Bdd.GherkinCompiler, Bdd.layerCucumber
  • guards: Bdd.isFeature, Bdd.isStepTimeoutError
  • models/errors: Bdd.Feature, Bdd.Scenario, Bdd.Step, Bdd.Report, Bdd.RunOptions, Bdd.RunError, Bdd.ParseError, Bdd.MatchError, Bdd.StepError, Bdd.StepTimeoutError

The error classes are also importable from effect-bdd/Errors.

Non-Goals

effect-bdd is not a Cucumber runtime clone. It does not include mutable worlds, hooks, screenshot/log attachments, snippet generation, retries, dry-run mode, parameter registries, generated chain code, or user-pluggable reporter APIs.

Provenance

effect-bdd started as the packages/bdd proposal in Effect-TS/effect-smol#2332 and now lives as a standalone community package.