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 adds the architecture and reporting that help your team understand what the system does and ship with confidence.
→ New to Serenity/JS? Start with Why Serenity/JS to see the full picture.
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/JS HTML Report for all stakeholders — developers, testers, and business audiences — with trend tracking, flaky test detection, and execution evidence |
| 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 22.22.2 and 24.15.0).
- 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/html-reporter @serenity-js/mocha
yarn add --dev @serenity-js/core @serenity-js/webdriverio @serenity-js/web @serenity-js/assertions @serenity-js/console-reporter @serenity-js/html-reporter @serenity-js/mocha
pnpm add --save-dev @serenity-js/core @serenity-js/webdriverio @serenity-js/web @serenity-js/assertions @serenity-js/console-reporter @serenity-js/html-reporter @serenity-js/mocha
→ 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/html-reporter', { specDirectory: './test/specs' } ],
+ ],
+ },
// ... keep your existing settings ...
- framework: 'mocha',
Next, add the following script to your package.json:
{
"scripts": {
+ "test": "wdio run ./wdio.conf.ts"
}
}
→ Learn more: WebdriverIO configuration | HTML reporter setup
Step 3: Run
npm test
That's it. Your existing tests run as before, but now produce a rich HTML report automatically at the end of the test run. Open reports/serenity-js/index.html in your browser to view it.
Pass any additional arguments directly to WebdriverIO. For example, to run a single spec file:
npx wdio run ./wdio.conf.ts --spec=test/specs/website.spec.ts
→ Learn more: Reporting overview | HTML Reporter | 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 test again. In the 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.
Go further
Now that you've seen the basics, here are complete projects you can use as reference or as a starting point for your own.
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 test — the HTML report lands in reports/serenity-js/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": {
"test": "wdio run ./wdio.conf.ts"
},
"devDependencies": {
"@serenity-js/assertions": "^3.45.9",
"@serenity-js/console-reporter": "^3.45.9",
"@serenity-js/core": "^3.45.9",
"@serenity-js/html-reporter": "^3.45.9",
"@serenity-js/mocha": "^3.45.9",
"@serenity-js/web": "^3.45.9",
"@serenity-js/webdriverio": "^3.45.9",
"@wdio/cli": "^9.0.0",
"@wdio/local-runner": "^9.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/html-reporter', { specDirectory: './test/specs' } ],
],
},
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?
Not if you use the Serenity/JS HTML Reporter — it produces rich reports with trend tracking and flaky test detection using pure Node.js. Java is only needed for the Serenity BDD Reporter.
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.