ScreenshotNeo

BlogGuides

Cucumber.js and Selenium Tutorial: Automate Browser Tests

Learn how to use Cucumber.js with Selenium WebDriver in JavaScript, from setup and step definitions to waits, cleanup, CI, and troubleshooting.

By the ScreenshotNeo team4 October 20269 min read

Cucumber.js describes browser behavior as readable scenarios; Selenium WebDriver drives the browser that executes those scenarios. Install @cucumber/cucumber and selenium-webdriver, write a Gherkin feature, connect each step to an asynchronous JavaScript function, wait for observable page state, and close the browser in an After hook. Selenium’s current JavaScript API requires Node.js 22 or later. Cucumber itself is not a browser automation tool; it works with tools such as Selenium WebDriver. Cucumber’s browser automation guide and the Selenium JavaScript API document these roles.

1. What Cucumber.js and Selenium each do

Cucumber.js is the Node.js implementation of Cucumber. It reads scenarios written in Gherkin, matches their Given/When/Then steps with JavaScript step definitions, and reports whether those steps passed. Its official package is @cucumber/cucumber.

Selenium WebDriver’s JavaScript binding, selenium-webdriver, sends commands to a browser through a browser-specific driver. Together, they let a test describe expected user-visible behavior in a feature file while JavaScript code opens pages, enters data, clicks controls, and checks the result.

Part Responsibility Example
Feature file Readable behavior and acceptance criteria “When I search for ‘cucumber’…”
Step definitions Map scenario phrases to test code Find the search box and submit a query
Selenium WebDriver Control the real browser and inspect page state Navigate, type, click, read title or text
Cucumber hooks Set up and clean up scenario resources Start and quit one browser session

2. Prerequisites and project setup

  1. Install Node.js 22 or later and npm. Confirm with node --version and npm --version.
  2. Make a project directory and initialize it.
  3. Install the official Cucumber.js package and Selenium’s JavaScript binding as development dependencies.
  4. Have Chrome available locally. Selenium Manager handles driver setup in Selenium’s documented quick-start flow, but browser availability, network policy, and CI restrictions can still affect startup.
mkdir cucumber-selenium-demo
cd cucumber-selenium-demo
npm init -y
npm install --save-dev @cucumber/cucumber selenium-webdriver

The installation steps follow the Cucumber.js installation guide and Selenium JavaScript API. Use the package manager and Node version your project standardizes on if this is an existing repository.

3. Write a feature scenario

Create features/search.feature. This example exercises a real search form and checks for the resulting page title. The page under test is a public search engine; for a production suite, point the scenario at an application and stable test data that your team controls.

Feature: Web search
  As a visitor
  I want to search for a term
  So that I can see relevant results

  Scenario: Submit a search query
    Given I am on the search page
    When I search for "cucumber selenium tutorial"
    Then the page title should contain "cucumber selenium tutorial"

4. Add browser setup, step definitions, waits, and cleanup

Create features/step_definitions/search.steps.js. The scenario-scoped Cucumber World holds the driver. Use regular functions where you access this; arrow functions have their own lexical this and cannot access the Cucumber World in the usual way. The hooks below make a fresh browser for each scenario and always attempt to close it afterward.

const assert = require('node:assert/strict');
const { Before, After, Given, When, Then, setDefaultTimeout } = require('@cucumber/cucumber');
const { Builder, Browser, By, until } = require('selenium-webdriver');

setDefaultTimeout(30_000);

Before(async function () {
  this.driver = await new Builder().forBrowser(Browser.CHROME).build();
});

After(async function () {
  if (this.driver) {
    await this.driver.quit();
    this.driver = undefined;
  }
});

Given('I am on the search page', async function () {
  await this.driver.get('https://www.google.com/');
  await this.driver.wait(until.elementLocated(By.name('q')), 10_000);
});

When('I search for {string}', async function (query) {
  const searchBox = await this.driver.findElement(By.name('q'));
  await searchBox.sendKeys(query);
  await searchBox.submit();
});

Then('the page title should contain {string}', async function (expectedText) {
  await this.driver.wait(
    until.titleContains(expectedText),
    10_000,
    'Expected the results page title to contain the search query'
  );
  const title = await this.driver.getTitle();
  assert.ok(title.toLowerCase().includes(expectedText.toLowerCase()),
    `Expected title to contain "${expectedText}", got "${title}"`);
});

Each Selenium operation returns a promise, so await navigation, element lookup, input, submission, waits, and reads. A navigation or click completing does not guarantee that a client-rendered page has reached the state your test needs. Wait for a specific condition such as an element appearing, becoming visible, or the title changing. Selenium’s JavaScript API documents WebDriver and wait conditions.

5. Run the test

Add a script to package.json or invoke Cucumber directly. The file path below tells Cucumber where the feature is; its conventional support directory discovery loads step definitions under features/step_definitions.

npx cucumber-js features

Or add this script to the scripts object in package.json:

{
  "scripts": {
    "test:e2e": "cucumber-js features"
  }
}

Then run npm run test:e2e. If your repository already has a Cucumber configuration file or custom support paths, follow that configuration; do not assume another project’s CLI options or config format apply.

6. Choose browsers and run against Selenium Grid

The local example selects Chrome explicitly with forBrowser(Browser.CHROME). You can select another browser supported by the Selenium binding and available in your environment, such as Firefox or Edge. Browser-specific installation and capabilities differ, so check the API documentation and your CI image.

For a remote Selenium server or Grid, point the builder at the server. Keep the URL configurable so local and CI runs can use different endpoints:

const { Builder, Browser } = require('selenium-webdriver');

const builder = new Builder().forBrowser(Browser.CHROME);
if (process.env.SELENIUM_REMOTE_URL) {
  builder.usingServer(process.env.SELENIUM_REMOTE_URL);
}
const driver = await builder.build();

The Selenium JavaScript API also documents the SELENIUM_BROWSER and SELENIUM_REMOTE_URL environment variables. Remote execution adds a network hop and a separately managed browser environment; it can centralize browser provisioning, but it does not remove the need to keep browser versions, capabilities, and server access configured.

7. Make scenarios reliable

  • Wait for conditions, not durations. Prefer driver.wait(until.elementLocated(...), timeout) to arbitrary sleeps. A fixed delay may be wasteful on a fast run and too short on a slow one.
  • Assert observable behavior. Check visible text, a URL, a page title, or a changed control state that represents the user’s outcome. Avoid coupling every step to internal implementation details.
  • Scope browser lifetime to a scenario. A fresh session limits state leaking between scenarios. Always call quit() in teardown, including after failed steps.
  • Use stable selectors. Prefer application-provided test attributes or robust accessible selectors over brittle positional CSS and generated class names.
  • Keep test data independent. Do not assume public websites, third-party services, or shared data remain unchanged. For repeatable acceptance tests, use an application environment and data under your control.
  • Keep timeouts purposeful. Set a reasonable scenario timeout for the slowest expected operation and use shorter explicit waits around individual conditions. A longer timeout does not fix a broken selector or unavailable service.

8. Capture a diagnostic screenshot on failure

A screenshot attached to a failed Cucumber scenario can make browser state easier to diagnose. Add this logic to the existing After hook before quitting the driver:

const { Status } = require('@cucumber/cucumber');

After(async function (scenario) {
  if (!this.driver) return;

  if (scenario.result?.status === Status.FAILED) {
    const png = await this.driver.takeScreenshot();
    await this.attach(Buffer.from(png, 'base64'), 'image/png');
  }

  await this.driver.quit();
  this.driver = undefined;
});

If you use this hook, replace the earlier cleanup hook rather than registering both and quitting the same session twice. Cucumber’s browser automation guide also describes attaching failure screenshots.

9. Troubleshooting common errors

Symptom Likely cause What to do
Cannot find module '@cucumber/cucumber' Dependencies are missing, installed outside this project, or command runs from a different directory. Run npm install --save-dev @cucumber/cucumber selenium-webdriver in the project root and invoke npx cucumber-js there.
Selenium install or syntax errors on an older Node release The current Selenium JavaScript API requires Node.js 22 or later. Upgrade Node, verify node --version, then reinstall dependencies if needed.
Browser fails to start or session cannot be created Browser is absent, incompatible, blocked by the environment, or driver setup cannot complete. Confirm the target browser is installed and runnable. Check network/proxy restrictions affecting Selenium Manager; configure an approved driver or remote Selenium server where required. Selenium Manager is not a guarantee for every environment.
Step ... is undefined Step text does not match, or Cucumber did not load the definition file. Compare the phrase in the feature with the expression in the step definition. Keep the file under the discovered support path or configure the project’s support paths.
element not found or NoSuchElementError The selector is wrong, page is not ready, or element is inside another frame or context. Inspect the page and selector, wait for element location, and switch into the correct frame or window when needed.
Timeout waiting for a title or element The expected state never appeared, the selector/expectation is wrong, or the application is slower than the wait. Inspect the captured screenshot and logs, verify the expected state manually, and adjust the specific condition timeout only if that state legitimately takes longer.
Click intercepted or element not interactable An overlay, animation, hidden state, or layout shift prevents interaction. Wait until the intended element is visible and enabled, handle the overlay deliberately, and prefer a stable user-facing interaction over JavaScript shortcuts.
Scenarios pass alone but fail in a suite Shared browser state, test data, or sessions are leaking between scenarios. Create and quit a driver per scenario, reset application data, and avoid order-dependent assumptions.
Browser process remains after a failure Teardown did not run to completion or cleanup threw before quit(). Keep cleanup in an After hook, check that driver exists, and avoid early returns before quitting. Record cleanup errors in CI output.

10. Performance, reliability, and cost

Browser tests start real browser sessions and perform networked page work, so their runtime depends on your application, browser, environment, and scenario count. Reuse a browser inside a scenario when appropriate, but isolate scenarios to avoid state coupling. Parallel execution can reduce elapsed time while increasing browser and machine resource use; configure worker counts to match available capacity and ensure test data is isolated.

For reliability, pin and maintain the Node/browser environment used by CI, make remote endpoints and credentials explicit configuration, and treat third-party pages as dependencies that can change or rate-limit. Use explicit waits and gather screenshots on failures. Avoid claiming a fixed runtime or reliability rate: these depend on the site and environment.

The libraries are open-source packages, but execution still consumes developer time and machine or hosted Grid capacity. Budget for CI minutes, browser infrastructure, maintenance of test data, and debugging. Selenium and Cucumber documentation do not establish a universal execution cost or speed.

11. Or skip the browser setup

If your task is to capture a page image or PDF rather than verify interactive behavior, ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API can capture a URL without setting up a Selenium browser session. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Cookie banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server gives AI agents screenshot tools. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots. These captures are useful for visual assets and page snapshots; they do not replace Selenium assertions for user flows. Sign up for 1,000 free screenshots a month, with no card required.

12. FAQ

Can Cucumber.js test a website without Selenium?

Cucumber.js needs a separate mechanism to interact with a browser. Selenium is one option; Cucumber’s browser automation guide lists other integrations too.

Do I need to write the scenario before the step definitions?

No. Teams often draft behavior and implementation together. The important part is that feature phrases match their definitions and describe outcomes clearly.

Can I use Selenium only for screenshots?

Yes, WebDriver can take screenshots from a live browser session. If the goal is only a URL-to-image or PDF capture, an API such as ScreenshotNeo can avoid maintaining browser setup.

Where can I check current package requirements?

Check the Cucumber.js installation page and the Selenium JavaScript API when setting up or upgrading, since runtime and package requirements can change.

Sources