VLearnVibium

BDD with Vibium and Cucumber

BDD with Vibium and Cucumber — map Gherkin Given/When/Then steps to Vibium browser actions, share state via the World, in JavaScript and Python.

By Pramod Dutta··15 min read·Verified with Vibium 26.2
▶ Animated overview · made with Remotion

To do BDD with Vibium and Cucumber, write Gherkin .feature files that describe behavior in Given/When/Then, then implement each step as a Cucumber step definition that drives Vibium's browser API. Vibium is not a BDD tool — it is the browser-automation layer. You pair it with @cucumber/cucumber in JavaScript (or behave/pytest-bdd in Python), launch the browser in a Before hook, store the handle on the Cucumber World (this.vibe), and call vibe.go(), vibe.find(), and el.click() inside your steps. Because Vibium's find() auto-waits for actionability, step definitions stay short and free of manual sleep() calls, which keeps Gherkin scenarios stable. The result is an executable specification: Given I am on the login page reads the same to a product owner and to the runner. This guide maps the full pipeline — feature file to steps to a live browser — with runnable code for both JS and Python.

How a Cucumber + Vibium test runs

What is BDD, and where does Vibium fit?

Behavior-Driven Development (BDD) expresses tests as plain-English scenarios that describe what the software should do, so developers, testers, and product owners share one readable contract. Cucumber parses those scenarios — written in a language called Gherkin — and calls the code you bind to each step.

Vibium sits one layer below Cucumber. Cucumber owns the Gherkin parsing, the step matching, and the report. Vibium owns the browser: it opens Chrome, finds elements, clicks, types, and captures screenshots. Neither replaces the other.

Here is the clean division of responsibility, which is the key mental model for the whole setup:

LayerToolResponsibility
SpecificationGherkin .feature fileHuman-readable behavior in Given/When/Then
Runner + glueCucumber (@cucumber/cucumber)Parse Gherkin, match steps, produce reports
Browser driverVibiumLaunch Chrome, find(), click(), type(), screenshot()
BrowserChrome for TestingAuto-downloaded by Vibium, no driver to match

Vibium is an AI-native browser-automation library built on WebDriver BiDi, created by Jason Huggins (co-creator of Selenium and Appium). It ships as a single Go binary that downloads its own Chrome, so a Cucumber suite needs no separate driver install — a common source of setup pain in older BDD stacks.

Why pair Vibium with Cucumber specifically?

Cucumber gives you a shared, plain-English contract; Vibium makes that contract stable and cheap to run. The pairing shines when the two properties combine: a .feature file that a product owner can read and approve, backed by an automation engine that auto-waits and needs zero driver management.

Three concrete wins come from this combination:

  • Living documentation. The .feature file is the acceptance criteria and it executes, so it never drifts from reality the way a wiki page does.
  • Stable steps. Vibium's find() waits for elements to be actionable before acting, so scenarios do not fail on timing the way manually-sleep()ed BDD suites do. See Vibium's waiting strategies for why.
  • Zero-config browser. No chromedriver to version-match against Chrome — the single Go binary handles it, which matters when a whole QA team runs the same suite.

That said, BDD is not free. Every scenario needs a step definition, and the Gherkin indirection only pays off when non-technical readers actually use it. We will cover when not to reach for Cucumber later.

How do I write a Gherkin feature file?

A Gherkin feature file describes one capability as a set of scenarios, each a sequence of Given (context), When (action), and Then (outcome) steps. Save it with a .feature extension — Cucumber discovers these automatically.

# features/login.feature
Feature: Account login
  As a registered user
  I want to log in
  So that I can reach my dashboard
 
  Scenario: Successful login with valid credentials
    Given I am on the login page
    When I log in as "alice" with password "secret"
    Then I should see the welcome message "Welcome, Alice"

Notice there is no code here and no mention of selectors, CSS, or Vibium. That is deliberate: the feature file is the language everyone agrees on, and the "how" lives entirely in the step definitions. Keep steps declarative — say what the user does, not which button they click.

How do I map Gherkin steps to Vibium in JavaScript?

Each Gherkin line matches a step definition — a function registered with Given, When, or Then from @cucumber/cucumber — and inside that function you call Vibium. First install both tools:

npm install --save-dev @cucumber/cucumber vibium

Launch the browser once per scenario in a Before hook and store it on the Cucumber World (the this object each step shares). Quit it in an After hook so every scenario starts clean.

// features/support/hooks.js
const { Before, After } = require('@cucumber/cucumber')
const { browser } = require('vibium/sync')
 
Before(function () {
  const bro = browser.launch({ headless: true })
  this.bro = bro
  this.vibe = bro.page()      // shared by every step in this scenario
})
 
After(function () {
  this.bro.close()
})

Now bind each Gherkin line to a Vibium action. The {string} placeholders capture the quoted values from the feature file and arrive as function arguments.

// features/step_definitions/login.steps.js
const { Given, When, Then } = require('@cucumber/cucumber')
const assert = require('node:assert')
 
Given('I am on the login page', function () {
  this.vibe.go('https://example.com/login')
})
 
When('I log in as {string} with password {string}', function (user, pass) {
  this.vibe.find({ label: 'Username' }).type(user)
  this.vibe.find({ label: 'Password' }).type(pass)
  this.vibe.find({ role: 'button', text: 'Sign in' }).click()
})
 
Then('I should see the welcome message {string}', function (message) {
  const text = this.vibe.find('.welcome').text()
  assert.ok(text.includes(message), `expected "${message}", got "${text}"`)
})

Run it with npx cucumber-js. Cucumber reads the feature, matches each line to a step, and your Vibium code drives a real Chrome session. The step bodies are short because find() auto-waits for the element to be actionable — there are no explicit waits to write.

Why store the browser on the Cucumber World?

The World is the this context Cucumber creates fresh for every scenario, and it is the correct place to share the browser because it guarantees isolation. Each scenario gets its own this, so one scenario's browser session can never leak into another's — the same guarantee good test isolation demands.

Contrast the two approaches:

ApproachState lives onIsolationVerdict
Module-level global let vibeShared module scopeLeaks across scenariosFragile — avoid
Cucumber World (this.vibe)Per-scenario contextFresh per scenarioRecommended

Because this is per-scenario, you can also stash intermediate data on it to pass values between steps — for example, capturing an order number in a When and asserting on it in a Then. This is how Gherkin steps, which cannot return values to each other, communicate.

When('I place the order', function () {
  this.vibe.find({ role: 'button', text: 'Place order' }).click()
  this.orderId = this.vibe.find('.order-id').text()   // saved on the World
})
 
Then('I see an order confirmation number', function () {
  assert.match(this.orderId, /^ORD-\d+$/)
})

One caveat: an arrow function () => {} does not bind Cucumber's World, so this.vibe would be undefined. Always use the function () {} form in step definitions and hooks.

How do I do BDD with Vibium in Python?

In Python you swap Cucumber for behave (or pytest-bdd) and call Vibium's sync API inside the step functions — the pattern is identical to JavaScript. Install both, then reuse the same .feature file since Gherkin is language-agnostic.

pip install behave vibium

behave puts hooks in an environment.py file. Launch the browser in before_scenario and store it on the shared context object, then quit in after_scenario.

# features/environment.py
from vibium import browser_sync as browser
 
 
def before_scenario(context, scenario):
    context.bro = browser.launch(headless=True)
    context.vibe = context.bro.page()
 
 
def after_scenario(context, scenario):
    context.bro.quit()

The context object is Python's equivalent of the Cucumber World — it is passed into every step and carries shared state. Bind each Gherkin line with the @given, @when, and @then decorators.

# features/steps/login_steps.py
from behave import given, when, then
 
 
@given('I am on the login page')
def step_open_login(context):
    context.vibe.go("https://example.com/login")
 
 
@when('I log in as "{user}" with password "{password}"')
def step_login(context, user, password):
    context.vibe.find(label="Username").type(user)
    context.vibe.find(label="Password").type(password)
    context.vibe.find(role="button", text="Sign in").click()
 
 
@then('I should see the welcome message "{message}"')
def step_check_welcome(context, message):
    text = context.vibe.find(".welcome").text()
    assert message in text, f'expected "{message}", got "{text}"'

Run it with behave. Because Vibium's Python client mirrors the JS API — go(), find(), type(), click(), text(), screenshot(), quit() — the shape of your steps is the same in both ecosystems. See the login automation walkthrough for the raw script this wraps.

How should I structure a BDD Vibium project?

Group files by role: .feature files describe behavior, step definitions bind lines to actions, hooks manage the browser lifecycle, and page objects hide the selectors. A predictable layout keeps a growing suite navigable.

features/
  login.feature              # Gherkin scenarios
  checkout.feature
  step_definitions/
    login.steps.js           # Given/When/Then → Vibium
    checkout.steps.js
  support/
    hooks.js                 # Before/After: launch + quit
    world.js                 # optional custom World
  pages/
    login_page.js            # locators + actions (POM)

The most important refinement is to keep raw selectors out of step definitions and put them in page objects. A step should read like the Gherkin it implements, delegating the "how" to a page class.

// features/pages/login_page.js
class LoginPage {
  constructor(vibe) { this.vibe = vibe }
 
  open() { this.vibe.go('https://example.com/login'); return this }
 
  login(user, pass) {
    this.vibe.find({ label: 'Username' }).type(user)
    this.vibe.find({ label: 'Password' }).type(pass)
    this.vibe.find({ role: 'button', text: 'Sign in' }).click()
  }
}
module.exports = { LoginPage }
// features/step_definitions/login.steps.js
const { Given, When } = require('@cucumber/cucumber')
const { LoginPage } = require('../pages/login_page')
 
Given('I am on the login page', function () {
  this.loginPage = new LoginPage(this.vibe).open()
})
 
When('I log in as {string} with password {string}', function (u, p) {
  this.loginPage.login(u, p)
})

Now a renamed button is a one-line fix in LoginPage, and every scenario that logs in keeps passing. This is the same maintainability payoff POM gives any suite, amplified because Gherkin steps are meant to stay stable. For the wider file-layout picture, see how to structure a Vibium test suite.

How do I run the same scenario over many inputs?

Use a Scenario Outline with an Examples table to run one scenario across many data rows without duplicating steps. Cucumber substitutes each row's values into the <placeholder> slots and runs the scenario once per row, which is how BDD handles data-driven cases while keeping the Gherkin readable.

# features/login.feature
  Scenario Outline: Login validation messages
    Given I am on the login page
    When I log in as "<user>" with password "<pass>"
    Then I should see the message "<message>"
 
    Examples:
      | user  | pass    | message                  |
      | alice | secret  | Welcome, Alice           |
      | alice | wrong   | Invalid password         |
      |       | secret  | Username is required     |

The step definitions do not change at all — they already accept the captured strings. The single When I log in as {string} with password {string} step drives Vibium three times with three input sets, and each row becomes its own line in the report. This keeps a table of cases in one readable place instead of three near-identical scenarios, and it is where BDD's data tables genuinely beat copy-pasted plain scripts.

For a single step that needs a block of structured data — say a registration form with many fields — pass a Gherkin data table to one step and iterate it in the definition:

    When I register with:
      | field    | value            |
      | name     | Jane Doe         |
      | email    | jane@example.com |
      | zip      | 60601            |
When('I register with:', function (dataTable) {
  const data = dataTable.rowsHash()   // { name: 'Jane Doe', email: ..., zip: ... }
  this.vibe.find({ label: 'Name' }).type(data.name)
  this.vibe.find({ label: 'Email' }).type(data.email)
  this.vibe.find({ label: 'ZIP' }).type(data.zip)
})

This keeps a wide form out of the scenario prose while still driving each field through Vibium's semantic find() selectors, which stay readable and resist cosmetic UI change.

How do I run only a subset of scenarios?

Tag scenarios with @name labels in Gherkin, then filter runs by tag so you can execute just the smoke set, just the checkout journeys, or everything. Tags are how a BDD suite scales — you keep hundreds of scenarios but run the slice that matters for a given push.

  @smoke @login
  Scenario: Successful login with valid credentials
    Given I am on the login page
    When I log in as "alice" with password "secret"
    Then I should see the welcome message "Welcome, Alice"

Run only the smoke scenarios with a tag expression:

# JavaScript
npx cucumber-js --tags "@smoke"
 
# Python (behave)
behave --tags=@smoke

Tags also let you scope hooks, which pairs neatly with Vibium. A Before hook tagged @authenticated can log a user in before the scenario runs, so those scenarios skip repeating the login steps:

const { Before } = require('@cucumber/cucumber')
const { LoginPage } = require('../pages/login_page')
 
Before({ tags: '@authenticated' }, function () {
  new LoginPage(this.vibe).open().login('alice', 'secret')
})

Now any scenario tagged @authenticated starts already logged in, driven by the same Vibium page object your steps use. This is the standard way to run a fast smoke lane in CI while keeping the full regression suite for nightly runs.

How do I capture a screenshot when a scenario fails?

Attach a Vibium screenshot in an After hook that fires on failure, so a red scenario leaves visual evidence. Cucumber passes the scenario result into the hook, and Vibium's screenshot() returns PNG bytes you can attach to the report or write to disk.

const { After, Status } = require('@cucumber/cucumber')
 
After(function (scenario) {
  if (scenario.result.status === Status.FAILED) {
    const png = this.vibe.screenshot({ fullPage: true })
    this.attach(png, 'image/png')   // embeds in the Cucumber HTML report
  }
  this.bro.close()
})

In Python behave, do the same in after_scenario by checking scenario.status and writing the PNG to a file. Either way, a failing acceptance test hands you a picture of exactly what the page looked like — far faster than re-reading the Gherkin. For the full capture options, see taking a screenshot, and for running this headless on a runner, see Vibium in CI/CD with GitHub Actions.

Given/When/Then to Vibium: the cheat sheet

Each Gherkin keyword maps to a category of Vibium call, and internalizing this table makes writing steps mechanical. Given sets up state, When performs the action under test, and Then reads state back to assert on it.

Gherkin stepIntentTypical Vibium call
Given I am on a pageNavigate / arrangethis.vibe.go(url)
Given I am logged inArrange via page objectnew LoginPage(this.vibe).login(...)
When I click / typeActfind({...}).click() / .type(text)
When I submit the formActfind({ role: 'button' }).click()
Then I see textAssert on contentfind('.sel').text() + assertion
Then the field shows a valueAssert on inputfind('#id').value()
Then the element is visibleAssert on statefind('.sel').isVisible()

A useful rule of thumb: When steps mutate the page, Then steps only read it. Keeping assertions out of When and actions out of Then makes scenarios easier to reason about and reuse.

When should I not use Cucumber with Vibium?

Skip Gherkin when no non-technical person will ever read the scenario, because the step-definition indirection is pure overhead if you are the only audience. BDD earns its keep on shared acceptance criteria — critical user journeys a product owner signs off on — not on exhaustive edge-case coverage.

Use this decision guide:

SituationBetter choice
Acceptance criteria a stakeholder reads and approvesCucumber + Vibium
Critical user journey (login, checkout, signup)Cucumber + Vibium
Dozens of input-validation edge casesPlain Vibium script or unit-style test
A quick one-off scraping or smoke checkPlain Vibium script
Team with no non-technical readers of testsPlain Vibium + a test runner

For the low-level cases, a plain Vibium script is faster to write and read than a feature file plus its glue. Many teams run a small, curated set of Cucumber journeys for the shared contract and a larger body of ordinary Vibium tests underneath — that mix gives you readable acceptance specs without paying the Gherkin tax on every assertion. If you have never written a Vibium script, start with installing Vibium and the core find() reference.

Common gotchas when wiring Vibium into Cucumber

Most first-run problems trace to three mistakes, all easy to avoid once named. Getting these right makes the suite reliable from the start.

  • Arrow functions break the World. Given('...', () => {...}) leaves this.vibe undefined. Use function () {} so Cucumber can bind its per-scenario this.
  • Launching the browser in the wrong place. Launch in Before/before_scenario, not at module top level, or scenarios will share one browser and leak state. One browser per scenario keeps them isolated.
  • Adding manual sleeps. Do not paper over timing with page.wait(ms). Vibium's find() already waits for actionability; unnecessary sleeps slow the suite and hide real problems. See waiting strategies.

A fourth, subtler one: keep business logic out of hooks. Hooks are for lifecycle — launch, quit, screenshot-on-fail — while the behavior belongs in steps so the .feature file stays the single source of truth. Respect that boundary and your BDD suite stays as readable as the Gherkin promises.

Next steps

Frequently asked questions

How do I use Vibium with Cucumber for BDD?

Write Gherkin feature files that describe behavior in Given/When/Then, then implement each step as a Cucumber step definition that drives Vibium. Launch the browser in a Before hook, store it on the Cucumber World, call vibe.go(), vibe.find(), and el.click() inside steps, and quit in an After hook.

Does Vibium have a built-in BDD or Cucumber runner?

No. Vibium is a browser-automation library, not a test runner. You bring your own BDD tool — @cucumber/cucumber for JavaScript or behave/pytest-bdd for Python — and call Vibium's API inside the step definitions. Vibium supplies the browser actions; Cucumber supplies the Gherkin parsing and reporting.

Where should I launch and close the browser in a Cucumber suite?

Launch in a Before hook and quit in an After hook so every scenario gets a clean browser. Store the launched handle on the Cucumber World (this.vibe) so all step definitions in that scenario share the same session. This mirrors setup/teardown and keeps scenarios isolated from each other.

How do I share state between Cucumber steps with Vibium?

Use the Cucumber World — the this context each step runs in. Assign the launched browser to this.vibe in a Before hook, and every Given/When/Then in the scenario can reach it. Store intermediate values, like a captured order number, as properties on this to pass data from one step to the next.

Can I write BDD Vibium tests in Python?

Yes. Use behave or pytest-bdd for the Gherkin layer and call Vibium's sync API inside the step functions. Launch with browser_sync in a before_scenario hook, store it on context.vibe, and use context.vibe.find() in your @given, @when, and @then steps. The pattern matches the JavaScript version.

Should every test be a Cucumber scenario when using Vibium?

No. Reserve Gherkin for behavior that non-technical stakeholders read and agree on — critical user journeys and acceptance criteria. For low-level checks and edge cases, plain Vibium scripts or unit-style tests are faster to write and read. Use BDD where the shared, plain-English contract adds value.

Vibium is created by Jason Huggins. This is an independent tutorial — see the official Vibium site and GitHub repo for canonical docs.

Related guides