Skip to main content

Parallel Tests in a Spec File

Since v10, WebdriverIO can run the tests of one spec file at the same time. Every Mocha it() or Cucumber scenario gets its own browser window (a WebDriver BiDi browsing context) in one worker and one browser session. Use it for spec files with many independent tests: they finish faster, and you don't need more worker processes or browser sessions, because each test only pays for opening a window.

It is a form of browser pooling: the tests share a pool of windows in one browser, not a pool of browsers. If you come from Playwright, parallelMode: 'userContexts' is the closest equivalent of fullyParallel: true: every test gets its own isolated context. The difference is that Playwright spreads the tests of a file across worker processes, each with its own browser, while WebdriverIO runs them at the same time in one worker and one browser.

This is separate from maxInstances, which runs whole spec files in parallel, one worker and one session per spec. You can combine the two: maxInstances sets how many spec files run at the same time, and maxParallelContexts sets how many tests run at the same time inside each of those files.

Experimental

Parallel mode requires the experimental 'wdio:experimentalBiDiCommands' capability, which changes how element commands are implemented in every test, not only parallel ones. See Behavior changes of the BiDi element commands.

Enable parallel mode​

You need:

  • a WebDriver BiDi session (the default, unless you set 'wdio:enforceWebDriverClassic': true),
  • the 'wdio:experimentalBiDiCommands': true capability,
  • @wdio/mocha-framework or @wdio/cucumber-framework. Jasmine does not support parallel mode.

If the session does not use BiDi, or the capability is missing, WebdriverIO logs a warning and runs the tests one after another. Without parallelMode, nothing changes.

Mocha​

wdio.conf.ts
export const config: WebdriverIO.Config = {
specs: ['./test/specs/**/*.e2e.ts'],
maxInstances: 1,
capabilities: [{
browserName: 'chrome',
'wdio:experimentalBiDiCommands': true
}],
framework: 'mocha',
mochaOpts: {
ui: 'bdd',
timeout: 60000,
parallelMode: 'contexts',
maxParallelContexts: 4
}
}

The spec file needs no changes:

test/specs/pages.e2e.ts
import { browser, $, expect } from '@wdio/globals'

describe('webdriver.io', () => {
it('shows the API page', async () => {
await browser.url('https://webdriver.io/docs/api')
await expect($('h1')).toHaveText('Introduction')
})

it('shows the getting started page', async () => {
await browser.url('https://webdriver.io/docs/gettingstarted')
await expect($('h1')).toHaveText('Getting Started')
})
})

Both tests run at the same time, each in its own window. browser.url(), $() and expect() act on the window of the test that calls them.

Cucumber​

wdio.conf.ts
export const config: WebdriverIO.Config = {
specs: ['./features/**/*.feature'],
capabilities: [{
browserName: 'chrome',
'wdio:experimentalBiDiCommands': true
}],
framework: 'cucumber',
cucumberOpts: {
require: ['./features/step-definitions/**/*.ts'],
parallelMode: 'contexts',
maxParallelContexts: 4
}
}

Every scenario of a feature file runs in its own window. Profiles (cucumberOpts.profiles, cucumberOpts.file) and the tags, name, strict, dryRun, failFast, retry and retryTagFilter options work like in a sequential run.

Options​

OptionValuesDefaultDescription
parallelMode'contexts', 'userContexts'not set (sequential)Turns on parallel mode and picks how much tests share.
maxParallelContextsnumberos.cpus().lengthHow many tests run at the same time.

Set both in mochaOpts or in cucumberOpts.

maxParallelContexts sets the number of slots. Each slot runs one test at a time, opens the window for its next test while the current test runs, and closes the previous window in the background. Up to 3 × maxParallelContexts windows, plus the session's first window, can therefore be open for a short time: each slot can hold one live, one prefetched and one closing window. Parallel mode closes only the windows it opened.

Choose between contexts and userContexts​

'contexts''userContexts'
Window per testyesyes
Cookies, localStorage, service workersshared by all testsisolated per test (user context)
Loginonce, if the tests reuse the session cookiein every test
Driver supportany BiDi browserneeds browser.createUserContext() support, otherwise the run fails

Pick 'contexts' by default. Pick 'userContexts' when tests must not see each other's cookies or storage, for example tests that log in as different users. Like 'contexts', 'userContexts' falls back to a sequential run with a warning when the session is not WebDriver BiDi or 'wdio:experimentalBiDiCommands' is missing from the capabilities. If the driver does not support browser.createUserContext, the run fails rather than falling back.

browser.newWindow() opens the window in the default user context, so a window opened that way is outside the test's cookie and storage isolation.

What each test owns​

Inside a parallel test, these commands act on the test's own window:

  • navigation: url, back, forward, refresh, getUrl, getTitle
  • element commands: $, $$, shadow$, click, setValue, getText, the waitFor* commands, and others
  • execute, actions, addInitScript
  • cookies: getCookies, setCookies, deleteCookies use the test's cookie partition
  • mock(): a mock created in a parallel test only intercepts and records requests of that test's window and its frames, so two tests can mock the same URL
  • screenshots: saveScreenshot, takeElementScreenshot

Commands that throw in parallel mode​

Some commands act on the whole browser or the whole session, so they cannot target a single test's window. Inside a parallel test they throw instead of affecting another test:

CommandsWhyUse instead
getWindowSize, setWindowSizethe OS window is shareduse browser.setViewport() to size a test's page. Parallel tests share one OS window
throttleNetwork, throttleCPUthey apply to the whole browserrun those tests without parallel mode
closeWindow, switchToParentFramethey change the focused window, which all tests sharekeep each test in its own window
reloadSession, setTimeoutthey change the session that all tests sharecall them in the before hook in wdio.conf.ts
acceptAlert, dismissAlert, getAlertText, sendAlertTextalerts belong to a browsing contextcall them on the context browser.url() returns: const page = await browser.url('https://webdriver.io'); await page.acceptAlert(). Use page.acceptAlert(text) instead of sendAlertText(text). dismissAlert() and getAlertText() work on the context too
getComputedRole, getComputedLabelWebDriver BiDi cannot read themrun those tests without parallel mode

switchWindow was removed for all WebDriver BiDi sessions in v10, not only in parallel mode. It throws switchWindow` was removed for WebDriver BiDi sessions in WebdriverIO v10. Hold the browsing context returned by `browser.url()` or `browser.newWindow()`, or find one with `browser.browsingContexts()`. Keep the context object instead of switching to it.

WebdriverIO dismisses prompts automatically unless something listens to the dialog event, so register a listener (browser.on('dialog', () => {})) before you answer a prompt from a test.

Every other WebDriver Classic command also throws. Only status, getTimeouts and getWindowHandles are allowed. If a WebdriverIO command falls back to WebDriver Classic in parallel mode without you calling a Classic command, please report it.

Hooks and timeouts​

Mocha​

  • before() and after() in a spec file run once per test, not once per describe, because every test has its own window. Each test runs before, beforeEach, the test, afterEach and after in its window. Put setup that should run once per spec file in the beforeSuite / afterSuite hooks in wdio.conf.ts. These run once, outside any test window.
  • Only mochaOpts.timeout applies. this.timeout() in a test or hook throws, and it(...).timeout() or a describe timeout is ignored with a warning. See Mocha timeouts and the WebdriverIO hooks.
  • mochaOpts.bail and mochaOpts.retries are not supported and fail the run. this.retries() throws.
  • .only(), this.skip(), grep, fgrep and invert work like in a sequential run.

Cucumber​

  • BeforeAll / AfterAll and the beforeFeature / afterFeature hooks in wdio.conf.ts run once per worker, not once per feature. beforeFeature / afterFeature receive the uri and feature of the first feature file only, which matters when one worker runs several feature files, for example grouped specs. They run outside any scenario's window, so they must not use commands that need one.
  • Before / After, BeforeStep / AfterStep and the scenario and step hooks in wdio.conf.ts run for every scenario, in that scenario's window.
  • cucumberOpts.strict defaults to false in WebdriverIO. A pending step fails the scenario only with strict: true.

Both frameworks​

A test's window closes as soon as the test finishes. Don't start timers or promises that keep running after the test ends.

Behavior changes of the BiDi element commands​

'wdio:experimentalBiDiCommands': true swaps the implementation of several element commands in every test of the session, sequential or parallel:

  • addValue and clearValue dispatch synthetic DOM events, not trusted key events.
  • getAttribute follows the WebDriver rules for property and attribute values.
  • click is sent through the Actions API.

Run your suite once with the capability and without parallelMode to find tests that depend on the Classic behavior.

Reporters​

Reporters that extend @wdio/reporter report parallel tests under the right suite, and attach a command to the test whose browsing context sent it.

@wdio/allure-reporter and @wdio/junit-reporter don't support parallel mode yet. They track one current test per worker, so when tests overlap, Allure results, steps and attachments, and JUnit properties and worker logs, can land under the wrong test. Use them with sequential runs. With addConsoleLogs, @wdio/spec-reporter doesn't attribute the console output of overlapping tests reliably.

A custom reporter must not assume that a test ends before the next one starts. See Reporters and parallel tests.

Troubleshooting​

parallelMode: "contexts" requires 'wdio:experimentalBiDiCommands': true to be set in capabilities​

Add 'wdio:experimentalBiDiCommands': true to the capability. Without it, the tests run one after another. With Cucumber, the message reads Parallel mode (cucumberOpts.parallelMode: "contexts") requires "wdio:experimentalBiDiCommands" to be set in capabilities.

... is set but the browser session does not support WebDriver Bidi​

The session uses WebDriver Classic. Remove 'wdio:enforceWebDriverClassic': true from the capability, or use a browser and driver with WebDriver BiDi support. With Cucumber, the message reads Parallel mode (cucumberOpts.parallelMode: "contexts") requires a WebDriver Bidi session. Falling back to sequential execution.

parallelMode: "userContexts" requires browser.createUserContext and browser.removeUserContext, which this driver does not support​

The driver cannot create user contexts. Use parallelMode: 'contexts' or update the browser and driver. With Cucumber, the message reads cucumberOpts.parallelMode "userContexts" requires browser.createUserContext, which this driver does not provide. Use a driver with BiDi user context support or set parallelMode to "contexts" (shares cookies and storage between scenarios).

... is not supported inside parallel mode tests because ... or ... is not supported in parallel mode​

The test called a command from Commands that throw in parallel mode. Use the alternative from the table.

bail is not supported in parallel mode / retries is not supported in parallel mode​

Remove mochaOpts.bail or mochaOpts.retries, or run the spec file without parallelMode.

Suite-level hooks (before()/after()) run once per test in parallel mode​

The spec file has before() or after() hooks. They run for every test. Move setup that should run only once to beforeSuite / afterSuite in wdio.conf.ts.

Welcome! How can I help?

WebdriverIO AI Copilot