تخطى إلى المحتوى الرئيسي

Selenium DevTools

Selenium WebDriver adapter for WebdriverIO DevTools - brings the same visual debugging UI to any selenium-webdriver test, regardless of the test runner.

Works with Mocha, Jest, Cucumber, or plain node script.js - the plugin auto-detects the runner and wires test boundaries accordingly. No changes to your test code are needed beyond a single import.

Installation

npm install @wdio/selenium-devtools

Setup

Each block below is a complete, copy-paste-ready example including the DevTools.configure(...) call. Pick the runner you use, drop the snippet into your project, and run it.

Mocha

// tests/example.test.js
import { strict as assert } from 'node:assert'
import { Builder, By, until } from 'selenium-webdriver'
import { DevTools } from '@wdio/selenium-devtools'

DevTools.configure({
screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 }
})

describe('smoke test', function () {
let driver

before(async function () {
driver = await new Builder().forBrowser('chrome').build()
})

after(async function () {
if (driver) {
await driver.quit()
}
})

it('loads example.com and reads the heading', async function () {
await driver.get('https://example.com')
const heading = await driver.wait(until.elementLocated(By.css('h1')), 10000)
assert.equal(await heading.getText(), 'Example Domain')
})
})

Run it:

mocha --timeout 60000 tests/example.test.js

Alternative: skip the per-file import and use mocha --require @wdio/selenium-devtools to load the plugin once for the whole run.

Jest

// test/example.js
import { DevTools } from '@wdio/selenium-devtools'
import { Builder, By, until } from 'selenium-webdriver'

DevTools.configure({
screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 }
})

describe('login flow', () => {
let driver

beforeEach(async () => {
driver = await new Builder().forBrowser('chrome').build()
}, 60000)

afterEach(async () => {
if (driver) {
await driver.quit()
}
})

test('logs in with valid credentials', async () => {
await driver.get('https://the-internet.herokuapp.com/login')
await driver.findElement(By.id('username')).sendKeys('tomsmith')
await driver.findElement(By.id('password')).sendKeys('SuperSecretPassword!')
await driver.findElement(By.css('button[type="submit"]')).click()

await driver.wait(until.urlContains('/secure'), 10000)
const flash = await driver.findElement(By.id('flash'))
expect(await flash.getText()).toMatch(/You logged into a secure area/i)
}, 60000)
})

jest.config.json:

{
"testEnvironment": "node",
"testMatch": ["<rootDir>/test/example.js"],
"testTimeout": 60000,
"transform": {}
}

Run it (ESM needs the experimental flag):

NODE_OPTIONS=--experimental-vm-modules jest --config jest.config.json

Cucumber

Cucumber's split layout means three small files - one to load the plugin, one for World/hooks, and one for step definitions.

features/support/setup.js - load the plugin and configure once:

import { DevTools } from '@wdio/selenium-devtools'

DevTools.configure({
screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 }
})

features/support/world.js - driver lifecycle:

import {
setWorldConstructor,
World,
Before,
After,
setDefaultTimeout
} from '@cucumber/cucumber'
import { Builder } from 'selenium-webdriver'

setDefaultTimeout(60000)

class CustomWorld extends World {
constructor (options) {
super(options)
this.driver = null
}
}

setWorldConstructor(CustomWorld)

Before(async function () {
this.driver = await new Builder().forBrowser('chrome').build()
})

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

cucumber.json - wire the setup file in first so the plugin patches Selenium before any step runs:

{
"default": {
"import": [
"features/support/setup.js",
"features/support/world.js",
"features/support/steps.js"
],
"paths": ["features/*.feature"],
"format": ["progress"]
}
}

Run it:

cucumber-js --config cucumber.json

Plain Node script (no test runner)

If you run node tests/google.test.js directly there's no runner for the plugin to auto-hook. By default you get a single "Selenium Session" row in the dashboard. To get a named test boundary, call DevTools.startTest / endTest around your work:

// tests/google.test.js
import { DevTools } from '@wdio/selenium-devtools'
import { Builder, By, until, Key } from 'selenium-webdriver'

DevTools.configure({
screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 },
headless: false
})

async function run () {
DevTools.startTest('search Google for Selenium') // optional - names the test row

const driver = await new Builder().forBrowser('chrome').build()
try {
await driver.get('https://www.google.com')
const searchBox = await driver.findElement(By.name('q'))
await searchBox.sendKeys('Selenium WebDriver JavaScript', Key.ENTER)
await driver.wait(until.titleContains('Selenium'), 10000)
DevTools.endTest('passed')
} catch (err) {
DevTools.endTest('failed')
throw err
} finally {
await driver.quit()
}
}

run()
node tests/google.test.js

Only use startTest / endTest for plain Node scripts. Under Mocha / Jest / Cucumber the plugin already knows when each test starts and ends - calling these manually would create duplicate rows.

Configuration Options

OptionTypeDefaultDescription
portnumber3000Port for the DevTools backend server. Auto-incremented if already in use.
hostnamestring'localhost'Hostname the backend server binds to.
openUibooleantrueAuto-open the DevTools UI in a new Chrome window. Set false for CI.
captureScreenshotsbooleantrueCapture a screenshot after every WebDriver command.
headlessbooleanfalseRun the test browser headless (injects --headless=old). The DevTools UI window is unaffected.
screencastScreencastOptions{ enabled: false }Per-session .webm video recording. Options match the WebdriverIO Screencast page.
rerunCommandstringautoCommand template for per-test rerun. {{testName}} is substituted. Auto-derived from runner argv if omitted.
mode'live' | 'trace''live'live opens the DevTools UI; trace skips it and writes a portable artifact instead. See Trace Mode. Overrides openUi.
traceFormat'zip' | 'ndjson-directory''zip'Trace artifact layout. Only applies when mode: 'trace'.
traceGranularity'session' | 'spec' | 'test''session'One trace per session / spec file / test. 'test' writes each to test-results/<spec>-<title>-<browser>[-retryN]/trace.zip. Only applies when mode: 'trace'. See Trace Mode.
tracePolicy'on' | 'retain-on-failure' | 'retain-on-first-failure' | 'on-first-retry' | 'on-all-retries' | 'retain-on-failure-and-retries''on'Which traces to keep. Pairs with traceGranularity: 'test'. Only applies when mode: 'trace'.
filmstripbooleantrueRecord a dense, continuous screencast into the trace for frame-by-frame scrubbing in the player. Only applies when mode: 'trace'.
screenshot'off' | 'on' | 'only-on-failure''off'Trace mode + traceGranularity: 'test'. Per-test screenshot, attached inline to Allure (image/png) via allure-js-commons when an Allure runner adapter is active.
video'off' | TraceRetentionPolicy'off'Trace mode + traceGranularity: 'test'. Per-test screencast video, retained per the given policy, attached inline to Allure (video/webm) via allure-js-commons when an Allure runner adapter is active.
emitArtifactsManifestbooleanautoWrite the devtools-artifacts-<sessionId>.json manifest — the generic index reporters/CI consume to discover produced artifacts — next to the trace. Off by default; auto-enables when an allure-js-commons runtime is active. Trace mode only.
captureAssertionsbooleantrueCapture node:assert assertions (both passing and failing) as trace action rows. Set false to opt out.
DevTools.configure({
port: 3000,
hostname: 'localhost',
headless: false,
openUi: true
})

For CI, set both headless: true (hide the test browser) and openUi: false (don't try to open the dashboard window - CI environments have no display). The backend keeps running on the configured port so you can still open the UI later if needed.

Trace mode

Headless capture path — no DevTools UI window opens. At session end the adapter writes a portable trace-<sessionId>.zip (or directory) into a test-results/ folder (next to the resolved test / config directory), with the same shape as the WebdriverIO trace artifact.

DevTools.configure({
mode: 'trace',
traceFormat: 'ndjson-directory' // optional; default 'zip'
})

The backend port-bind, UI window, and screencast option are all skipped in trace mode. For the full feature reference (artifact contents, viewer, mobile testing, when to pick zip vs ndjson-directory), see the Trace Mode page.

Per-test artifacts and retention

At traceGranularity: 'test' each test gets its own artifact folder, and tracePolicy decides which are kept (e.g. retain-on-failure). In that mode you can also capture a per-test screenshot (PNG) and video (.webm), and enable a dense filmstrip recorded into the trace for frame-by-frame scrubbing. When an allure-js-commons runner adapter is active, per-test traces / screenshots / videos are attached inline to the Allure report (and emitArtifactsManifest auto-enables); otherwise they're written to test-results/ and recorded in the manifest.

DevTools.configure({
mode: 'trace',
traceGranularity: 'test',
tracePolicy: 'retain-on-failure',
filmstrip: true,
screenshot: 'only-on-failure',
video: 'retain-on-failure'
})

Viewing the trace

Open any trace .zip in the first-party player — the same DevTools UI in a dedicated player mode:

npx show-trace path/to/trace.zip      # in a project that installs the adapter
pnpm show-trace path/to/trace.zip # from the devtools monorepo

The show-trace bin ships with @wdio/selenium-devtools, so it's available in any project that installs it — no extra dependency. Because the Selenium adapter captures the page's DOM mutation stream and a per-command element / accessibility snapshot alongside each screenshot, a Selenium trace drives the player's full feature set — DOM time-travel, the A11y tab and pick-locator overlay, the Transcript tab with Copy-for-LLM, Cucumber Feature → Scenario → Step nesting, and the scrubbable timeline.

The trace uses a portable NDJSON schema, so the same .zip (or directory) also opens in other compatible trace viewers. See the Trace Player page for the full walkthrough.

Public API

import { DevTools } from '@wdio/selenium-devtools'

DevTools.configure(opts) // set runtime options (see above)
DevTools.startTest(name, meta?) // mark a named test boundary (plain Node scripts only)
DevTools.endTest('passed'|'failed'|'skipped'|'pending')

Under Mocha / Jest / Cucumber the plugin auto-hooks the runner's lifecycle, so you don't need startTest / endTest manually - calling them would create duplicate rows.

Examples

Working examples live in the repo's top-level examples/ directory. Build the workspace once (pnpm install && pnpm build), then run from the repo root. pnpm demo:selenium runs the default (Cucumber) example; the per-runner variants are:

DirectoryRunnerCommand
examples/selenium/mocha-test/Mochapnpm --filter @wdio/selenium-devtools example:mocha
examples/selenium/jest-test/Jestpnpm --filter @wdio/selenium-devtools example:jest
examples/selenium/cucumber-test/Cucumberpnpm demo:selenium

Features

The Selenium adapter provides the same DevTools UI experience as WebdriverIO. Every feature below is captured automatically with the base DevTools.configure({}) setup — no per-feature config (console + network stream via Selenium's BiDi handlers on Chrome ≥114, with an injected-collector fallback). Links go to each feature's full reference.

Screencast is the one feature with its own options (see Configuration Options):

DevTools.configure({ screencast: { enabled: true, quality: 70, maxWidth: 1280, maxHeight: 720 } })

How It Works

The plugin patches selenium-webdriver's Builder, WebDriver, and WebElement prototypes at import time:

  • Builder.build() - after construction, the driver is registered with the session capturer and the DevTools backend is started in a detached child process.
  • Every public WebDriver / WebElement method - wrapped with command capture (args + result + screenshot + call source).
  • WebDriver.quit() - an awaited cleanup hook flushes screencast encoding, WebSocket buffer, and final metadata before the original quit runs.

When BiDi is available (Chrome ≥114), console logs, JavaScript exceptions, and network events stream directly via the Selenium BiDi handlers. Otherwise the plugin falls back to an injected browser-side collector script.

The same injected collector also records the page's DOM mutation stream and a per-command element / accessibility snapshot, so a trace carries enough to rebuild the live DOM at each step (per-navigation mapping) — this is what powers the player's DOM time-travel and A11y tab rather than a screenshot-only replay.

Limitations

LimitationDetail
Cucumber leaf-step rerunCucumber's --name filter targets scenarios, not individual Gherkin steps. The dashboard's per-step rerun is disabled under Cucumber.
Headless mode caveatheadless: true injects --headless=old; --headless=new produces all-black CDP frames in the screencast.
Initial viewportThe dashboard's snapshot iframe falls back to 1280×800 until the first navigation completes and the browser-side collector reports the real viewport.

Welcome! How can I help?

WebdriverIO AI Copilot