Serenity/JS for WebdriverIO Users
Already using WebdriverIO? Serenity/JS plugs right into your existing setup — Mocha, Cucumber, or Jasmine, your choice.
You keep WebdriverIO's cross-browser support, WebDriver protocol compliance, and mobile testing capabilities. Serenity/JS brings the structure and reporting that matter once your test suite outgrows a handful of specs.
Why add Serenity/JS?
Serenity/JS works on top of WebdriverIO — your existing test code, browser configuration, parallel execution, and CI/CD pipeline all stay exactly as they are. You don't need to rewrite anything.
Here's how Serenity/JS helps you address common WebdriverIO challenges:
| Challenge | How Serenity/JS helps |
|---|---|
| Duplicated selectors and test logic | The Screenplay Pattern gives you composable, reusable Tasks that separate what from how |
| Reports show only pass/fail | Structured reports show every action, with timing and screenshots |
| Stakeholders can't read test reports | Serenity BDD reports generate living documentation for both technical and business audiences |
| Logic duplicated across API and UI tests | Screenplay Tasks work across interfaces |
| Slow UI-only test suites | Blended testing — use APIs for setup, UI only where it matters |
| Locked into one tool | Screenplay Tasks are portable — switch to Playwright without rewriting test logic |
Get started in 5 minutes
You don't have to adopt everything at once. The progression most teams go through looks like this:
- Add reporting (Steps 1–3) — configure the framework adapter and reporters. Existing tests get better reports immediately.
- Use Screenplay for new tests (Step 4) — use the actor model for new scenarios. Old tests stay untouched.
- Extract shared Tasks — refactor common workflows into reusable building blocks.
- Consider portability — if you ever need Playwright (or both), your Tasks move with you.
Step 0: Create a WebdriverIO project
Skip this step and proceed with the remaining steps from your project directory.
Create a new directory and run the WebdriverIO wizard:
mkdir my-serenity-project
cd my-serenity-project
npm init wdio@latest .
When prompted, select the following options to get a project with Serenity/JS pre-configured:
- Type of testing: E2E Testing
- Automation backend: local
- Environment: web
- Browser: Chrome
- Framework: Mocha with Serenity/JS (or Cucumber/Jasmine with Serenity/JS)
- Compiler: TypeScript
- Generate test files: yes
- Test file location: accept the defaults
- Test reporter: any (Serenity/JS configures its own reporting)
- Plugins/add-ons/services: none
This creates a fully configured Serenity/JS + WebdriverIO project with example tests, reporters, and a wdio.conf.ts ready to go.
If you selected a "with Serenity/JS" framework option in the wizard, Steps 1 and 2 are already done — skip straight to Step 3: Run.
Step 1: Install
You'll need a recent Node.js LTS version (Serenity/JS supports Node 20, 22, and 24), as well as Java 11+ for the Serenity BDD reports.
- npm
- Yarn
- pnpm
npm install --save-dev @serenity-js/core @serenity-js/webdriverio @serenity-js/web @serenity-js/assertions @serenity-js/console-reporter @serenity-js/serenity-bdd @serenity-js/mocha rimraf npm-failsafe
yarn add --dev @serenity-js/core @serenity-js/webdriverio @serenity-js/web @serenity-js/assertions @serenity-js/console-reporter @serenity-js/serenity-bdd @serenity-js/mocha rimraf npm-failsafe
pnpm add --save-dev @serenity-js/core @serenity-js/webdriverio @serenity-js/web @serenity-js/assertions @serenity-js/console-reporter @serenity-js/serenity-bdd @serenity-js/mocha rimraf npm-failsafe
→ Learn more: Full WebdriverIO installation guide | Why Serenity/JS?
Step 2: Configure
Update your wdio.conf.ts to use the Serenity/JS framework adapter. You need to make two changes:
- Set
frameworkto'@serenity-js/webdriverio' - Add the
serenityconfiguration block with your test runner and reporters
Lines marked with + should be added to your file; lines marked with - should be removed. Don't copy the +/- prefixes themselves.
+ import { WebdriverIOConfig } from '@serenity-js/webdriverio';
- export const config = {
+ export const config: WebdriverIOConfig = {
+ framework: '@serenity-js/webdriverio',
+ serenity: {
+ runner: 'mocha',
+ crew: [
+ '@serenity-js/console-reporter',
+ [ '@serenity-js/serenity-bdd', { specDirectory: './test/specs' } ],
+ [ '@serenity-js/core:ArtifactArchiver', {
+ outputDirectory: './target/site/serenity'
+ } ],
+ ],
+ },
// ... keep your existing settings ...
- framework: 'mocha',
Next, add the following scripts to your package.json:
{
"scripts": {
+ "serenity": "failsafe serenity:clean wdio [...] serenity:report",
+ "serenity:clean": "rimraf target",
+ "wdio": "wdio run ./wdio.conf.ts",
+ "serenity:report": "serenity-bdd run --features='./test/specs'"
}
}
→ Learn more: WebdriverIO configuration | Serenity BDD reporter setup | Using npm-failsafe
Step 3: Run
npm run serenity
That's it. Your existing tests run as before, but now produce a rich HTML report in target/site/serenity/. Open target/site/serenity/index.html in your browser to view it.
The [...] wildcard in the serenity script is provided by npm-failsafe and passes any arguments you provide directly to WebdriverIO. For example, to run a single spec file:
npm run serenity -- --spec=test/specs/website.spec.ts
→ Learn more: Reporting overview | Serenity BDD reports
Step 4 (optional): Write your first Screenplay test
Steps 1–3 give you better reporting with zero changes to your test logic. When you're ready to see what the Screenplay Pattern looks like in practice, add a new test file:
import { describe, it } from 'mocha';
import { actorCalled } from '@serenity-js/core';
import { Ensure, includes } from '@serenity-js/assertions';
import { Navigate, Page } from '@serenity-js/web';
describe('Website', () => {
it('should have a descriptive title', async () => {
await actorCalled('Serena').attemptsTo(
Navigate.to('https://serenity-js.org/'),
Ensure.that(
Page.current().title().describedAs('current page title'),
includes('Serenity/JS')
),
);
});
});
Run npm run serenity again. In the Serenity BDD report you'll now see each interaction (Navigate.to, Ensure.that) listed as a separate step with timing — giving you a clear activity breakdown without any extra configuration.
This test uses actorCalled and Screenplay interactions instead of WebdriverIO's browser object directly. The key differences:
actorCalled('Serena')creates a named actor — the actor has a browser, but can also interact with REST APIs, multiple browser contexts, etc.Navigate.to()andEnsure.that()are composable interactions that show up as named steps in your reports.- Screenplay Tasks are portable — the same test code works with WebdriverIO and Playwright, only the config differs.
→ Learn more: Screenplay Pattern | Your first web scenario
For a more advanced example showing composable Tasks, the Lean Page Objects pattern, and masked credentials, see the Complete example below.
Screenplay code is identical whether you use Playwright or WebdriverIO — only the config differs. See Serenity/JS with Playwright.
Reference projects
Complete example
A self-contained project you can copy in full. It includes:
test/specs/website.spec.ts— a simple Screenplay test usingNavigateandEnsure(same pattern as Step 4)test/specs/swag-labs.spec.ts— a more advanced test interacting with the Swag Labs demo website, using composableTaskdefinitions and the Lean Page Objects pattern
Run npm install then npm run serenity — the Serenity BDD HTML report lands in target/site/serenity/index.html.
The password secret_sauce is hard-coded in these examples to keep them simple and self-contained. In real-world tests, load credentials from environment variables or a password vault to avoid committing secrets to your repository.
- package.json
- wdio.conf.ts
- test/specs/website.spec.ts
- test/specs/swag-labs.spec.ts
{
"name": "my-serenity-js-webdriverio-project",
"version": "1.0.0",
"scripts": {
"serenity": "failsafe serenity:clean wdio [...] serenity:report",
"serenity:clean": "rimraf target",
"wdio": "wdio run ./wdio.conf.ts",
"serenity:report": "serenity-bdd run --features='./test/specs'"
},
"devDependencies": {
"@serenity-js/assertions": "^3.43.2",
"@serenity-js/console-reporter": "^3.43.2",
"@serenity-js/core": "^3.43.2",
"@serenity-js/mocha": "^3.43.2",
"@serenity-js/serenity-bdd": "^3.43.2",
"@serenity-js/web": "^3.43.2",
"@serenity-js/webdriverio": "^3.43.2",
"@wdio/cli": "^9.0.0",
"@wdio/local-runner": "^9.0.0",
"npm-failsafe": "^1.4.0",
"rimraf": "^6.0.0",
"typescript": "^5.7.0",
"webdriverio": "^9.0.0"
}
}
import { WebdriverIOConfig } from '@serenity-js/webdriverio';
export const config: WebdriverIOConfig = {
framework: '@serenity-js/webdriverio',
serenity: {
runner: 'mocha',
crew: [
'@serenity-js/console-reporter',
[ '@serenity-js/serenity-bdd', { specDirectory: './test/specs' } ],
[ '@serenity-js/core:ArtifactArchiver', {
outputDirectory: './target/site/serenity'
} ],
],
},
specs: [ './test/specs/**/*.spec.ts' ],
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': { args: ['--headless'] },
}],
autoCompileOpts: {
tsNodeOpts: { project: './tsconfig.json' },
},
};
import { describe, it } from 'mocha';
import { actorCalled } from '@serenity-js/core';
import { Ensure, includes } from '@serenity-js/assertions';
import { Navigate, Page } from '@serenity-js/web';
describe('Website', () => {
it('should have a descriptive title', async () => {
await actorCalled('Serena').attemptsTo(
Navigate.to('https://serenity-js.org/'),
Ensure.that(
Page.current().title().describedAs('current page title'),
includes('Serenity/JS')
),
);
});
});
import { describe, it } from 'mocha';
import { actorCalled, Masked, Task } from '@serenity-js/core';
import { Ensure, equals } from '@serenity-js/assertions';
import { Navigate, Click, Enter, Text, PageElement, By } from '@serenity-js/web';
describe('Swag Labs', () => {
it('should let a standard user complete checkout', async () => {
await actorCalled('Alice').attemptsTo(
Navigate.to('https://www.saucedemo.com/'),
Authenticate.withCredentials('standard_user', 'secret_sauce'),
Inventory.productCalled('Sauce Labs Backpack').addToCart(),
Checkout.completeWith({
firstName: 'Alice',
lastName: 'Smith',
postalCode: '90210',
}),
Ensure.that(Checkout.confirmationHeading(), equals('Thank you for your order!')),
);
});
});
// --- Screenplay Tasks ---
class Authenticate {
private static usernameField =
PageElement.located(By.css('[data-test="username"]')).describedAs('username field');
private static passwordField =
PageElement.located(By.css('[data-test="password"]')).describedAs('password field');
private static loginButton =
PageElement.located(By.css('[data-test="login-button"]')).describedAs('login button');
static withCredentials = (username: string, password: string) =>
Task.where(`#actor logs in as ${ username }`,
Enter.theValue(username).into(Authenticate.usernameField),
Enter.theValue(Masked.valueOf(password)).into(Authenticate.passwordField),
Click.on(Authenticate.loginButton),
);
}
class ProductCard {
constructor(private name: string) {}
private card = PageElement.located(By.css(
`[data-test="inventory-item"]:has([data-test="inventory-item-name"]:text("${ this.name }"))`
)).describedAs(`product card for "${ this.name }"`);
private inventoryItemPrice =
PageElement.located(By.css('[data-test="inventory-item-price"]'))
.of(this.card).describedAs(`price of "${ this.name }"`);
private addToCartButton =
PageElement.located(By.css('[data-test^="add-to-cart"]'))
.of(this.card).describedAs(`"Add to cart" button for "${ this.name }"`);
price = () =>
Text.of(this.inventoryItemPrice);
addToCart = () =>
Task.where(`#actor adds "${ this.name }" to the cart`,
Click.on(this.addToCartButton),
);
}
class Inventory {
static productCalled = (name: string) => new ProductCard(name);
}
class Cart {
private static cartLink =
PageElement.located(By.css('[data-test="shopping-cart-link"]')).describedAs('shopping cart link');
private static checkoutButton =
PageElement.located(By.css('[data-test="checkout"]')).describedAs('checkout button');
static open = () =>
Task.where('#actor opens the shopping cart',
Click.on(Cart.cartLink),
);
static checkout = () =>
Task.where('#actor proceeds to checkout',
Cart.open(),
Click.on(Cart.checkoutButton),
);
}
class Checkout {
private static firstNameField =
PageElement.located(By.css('[data-test="firstName"]')).describedAs('first name');
private static lastNameField =
PageElement.located(By.css('[data-test="lastName"]')).describedAs('last name');
private static postalCodeField =
PageElement.located(By.css('[data-test="postalCode"]')).describedAs('postal code');
private static continueButton =
PageElement.located(By.css('[data-test="continue"]')).describedAs('continue button');
private static finishButton =
PageElement.located(By.css('[data-test="finish"]')).describedAs('finish button');
private static confirmationHeader =
PageElement.located(By.css('[data-test="complete-header"]')).describedAs('confirmation heading');
static completeWith = (info: { firstName: string; lastName: string; postalCode: string }) =>
Task.where(`#actor completes checkout`,
Cart.checkout(),
Enter.theValue(info.firstName).into(Checkout.firstNameField),
Enter.theValue(info.lastName).into(Checkout.lastNameField),
Enter.theValue(info.postalCode).into(Checkout.postalCodeField),
Click.on(Checkout.continueButton),
Click.on(Checkout.finishButton),
);
static confirmationHeading = () =>
Text.of(Checkout.confirmationHeader);
}
Project templates
If you'd rather start from a fully configured project than add Serenity/JS to an existing one, use one of these GitHub template repositories. Each comes with CI configuration, example tests, and live reports you can preview.
| Stack | Live Report |
|---|---|
| Cucumber + WebdriverIO | Serenity BDD |
| Mocha + WebdriverIO | Serenity BDD |
| Jasmine + WebdriverIO | Serenity BDD |
FAQ
Does it slow down my tests?
No. Serenity/JS adds negligible overhead. Your tests still run on WebdriverIO's engine with the same parallelism.
Do I have to rewrite all my tests?
No. You can mix Screenplay and non-Screenplay tests in the same project. Start with reporting and introduce Screenplay gradually for new tests.
Can I switch to Playwright later?
Yes. Screenplay Tasks import from @serenity-js/web and @serenity-js/core — not from WebdriverIO directly. If you ever want to move to Playwright (or use both), your Tasks are portable. Only the configuration changes.
Do I need Java?
Yes, for the Serenity BDD HTML reports (Java 11+). If you'd rather stay Java-free, use the console reporter alone.
Check the Troubleshooting guide for solutions to common issues with reports, screenshots, and configuration.
Next steps
- Why Serenity/JS? — same test at three levels of abstraction
- 15-minute tutorial — build your first Screenplay test
- WebdriverIO integration guide — detailed setup and configuration
- Project templates — all available starter projects
- Troubleshooting — solutions to common issues
Serenity/JS works just as well with Playwright Test. See Serenity/JS with Playwright.