Serenity/JS for Playwright Users
Already using Playwright Test? Good โ Serenity/JS is built to work on top of it, not instead of it.
You keep Playwright's speed, auto-waiting, tracing, and parallel execution. Serenity/JS adds 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 Playwright Test โ your existing test code, auto-waiting, parallel execution, Trace Viewer, UI Mode, 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 Playwright Test challenges:
| Challenge | How Serenity/JS helps |
|---|---|
| Duplicated selectors and test logic | The Screenplay Pattern gives you composable, reusable Tasks that separate what from how |
| Hard to tell what a test did | Structured reports show every action, with timing and screenshots |
| Multi-user workflows are hard to implement | Multi-actor support is built in |
| 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 integration tools and test runners 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โ4) โ configure reporters, change one import. Existing tests get better reports immediately.
- Use Screenplay for new tests (Step 5) โ use the actor model for new scenarios. Old tests stay untouched.
- Extract shared Tasks โ refactor common workflows into reusable building blocks.
- Multi-actor and cross-interface โ add multi-user workflows or mixed UI + API interactions.
Step 0: Create a Playwright Test projectโ
Skip this step and proceed with the remaining steps from your project directory.
Create a new directory and scaffold a Playwright Test project inside it:
mkdir my-serenity-project
cd my-serenity-project
npm init --yes playwright@latest -- --quiet
This creates a TypeScript Playwright Test project with tests in the tests/ directory, a playwright.config.ts config file, and example tests.
Drop the --quiet flag to get interactive prompts where you can choose your preferred language, test directory, and browsers:
npm init playwright@latest
Step 1: Installโ
To use Serenity/JS, 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/console-reporter @serenity-js/playwright @serenity-js/playwright-test @serenity-js/rest @serenity-js/web @serenity-js/serenity-bdd rimraf npm-failsafe
yarn add --dev @serenity-js/core @serenity-js/console-reporter @serenity-js/playwright @serenity-js/playwright-test @serenity-js/rest @serenity-js/web @serenity-js/serenity-bdd rimraf npm-failsafe
pnpm add --save-dev @serenity-js/core @serenity-js/console-reporter @serenity-js/playwright @serenity-js/playwright-test @serenity-js/rest @serenity-js/web @serenity-js/serenity-bdd rimraf npm-failsafe
โ Learn more: Full installation guide | Why Serenity/JS?
Step 2: Configureโ
Update your playwright.config.ts to add the Serenity/JS reporter. You need to make three changes:
- Add the
SerenityFixturesandSerenityWorkerFixturesimport - Add type parameters to
defineConfig - Replace the
reporteroption with the Serenity/JS reporter configuration
Lines marked with + should be added to your file; lines marked with - should be removed. Don't copy the +/- prefixes themselves.
- import { defineConfig, devices } from '@playwright/test';
+ import { defineConfig, devices } from '@playwright/test';
+ import { SerenityFixtures, SerenityWorkerFixtures } from '@serenity-js/playwright-test';
- export default defineConfig({
+ export default defineConfig<SerenityFixtures, SerenityWorkerFixtures>({
testDir: './tests',
// ... keep your existing settings ...
- reporter: 'html',
+ reporter: [
+ [ 'line' ],
+ [ '@serenity-js/playwright-test', {
+ crew: [
+ '@serenity-js/console-reporter',
+ [ '@serenity-js/serenity-bdd', {
+ specDirectory: './tests'
+ } ],
+ [ '@serenity-js/core:ArtifactArchiver', {
+ outputDirectory: './reports/serenity'
+ } ],
+ ]
+ }]
+ ],
Next, add the following scripts to your package.json:
{
"scripts": {
+ "clean": "rimraf target",
+ "test": "failsafe clean test:execute [...] test:report",
+ "test:execute": "npx playwright test",
+ "test:report": "serenity-bdd run --features='./tests' --source='./reports/serenity' --destination='./reports/serenity'"
}
}
โ Learn more: Configuration options | Serenity BDD reporter setup | Using npm-failsafe
Step 3: Change one importโ
In your test files:
- import { test, expect } from '@playwright/test';
+ import { test, expect } from '@serenity-js/playwright-test';
โ Learn more: Writing tests with Serenity/JS | Multi-actor scenarios
Step 4: Runโ
npm test
That's it. Your existing tests run as before, but now produce a rich HTML report in reports/serenity/. Open reports/serenity/index.html in your browser to view it.
The [...] wildcard in the test script is provided by npm-failsafe and passes any arguments you provide directly to Playwright Test. For example, to run a single spec file:
npm test -- --spec=tests/website.spec.ts
Or to run tests matching a specific name:
npm test -- --grep="descriptive title"
โ Learn more: Reporting overview | Serenity BDD reports
Step 5 (optional): Write your first Screenplay testโ
Steps 1โ4 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 '@serenity-js/playwright-test';
import { Ensure, includes } from '@serenity-js/assertions';
import { Navigate, Page } from '@serenity-js/web';
describe('Website', () => {
it('should have a descriptive title', async ({ actor }) => {
await actor.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 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 does the same thing as the generated example.spec.ts, but uses the actor fixture and Screenplay interactions instead of page directly. The key differences:
actorreplacespageas the entry point โ the actor has a page, 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.describeanditcome from@serenity-js/playwright-testโ they work like Playwright'stest()but give you richer reporting structure.
โ 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.
See the full installation guide for advanced options, or the architecture overview to understand how the pieces fit together.
Reference projectsโ
Complete exampleโ
A self-contained project you can copy in full. It includes:
tests/website.spec.tsโ a simple Screenplay test usingNavigateandEnsure(same pattern as Step 5)tests/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 Serenity BDD HTML report lands in reports/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
- playwright.config.ts
- tests/website.spec.ts
- tests/swag-labs.spec.ts
{
"name": "my-serenity-js-project",
"version": "1.0.0",
"scripts": {
"clean": "rimraf target",
"test": "failsafe clean test:execute [...] test:report",
"test:execute": "npx playwright test",
"test:report": "serenity-bdd run --features='./tests' --source='./reports/serenity' --destination='./reports/serenity'"
},
"devDependencies": {
"@serenity-js/assertions": "^3.43.2",
"@serenity-js/console-reporter": "^3.43.2",
"@serenity-js/core": "^3.43.2",
"@serenity-js/playwright": "^3.43.2",
"@serenity-js/playwright-test": "^3.43.2",
"@serenity-js/serenity-bdd": "^3.43.2",
"@serenity-js/web": "^3.43.2",
"@playwright/test": "~1.60.0",
"npm-failsafe": "^1.4.0",
"rimraf": "^6.0.0",
"typescript": "^5.7.0"
}
}
import { defineConfig } from '@playwright/test';
import { SerenityFixtures, SerenityWorkerFixtures } from '@serenity-js/playwright-test';
export default defineConfig<SerenityFixtures, SerenityWorkerFixtures>({
reporter: [
[ 'line' ],
[ '@serenity-js/playwright-test', {
crew: [
'@serenity-js/console-reporter',
[ '@serenity-js/serenity-bdd', {
specDirectory: './tests'
} ],
[ '@serenity-js/core:ArtifactArchiver', {
outputDirectory: './reports/serenity'
} ],
]
}]
],
});
import { describe, it } from '@serenity-js/playwright-test';
import { Ensure, includes } from '@serenity-js/assertions';
import { Navigate, Page } from '@serenity-js/web';
describe('Website', () => {
it('should have a descriptive title', async ({ actor }) => {
await actor.attemptsTo(
Navigate.to('https://serenity-js.org/'),
Ensure.that(
Page.current().title().describedAs('current page title'),
includes('Serenity/JS')
),
);
});
});
import { describe, it } from '@serenity-js/playwright-test';
import { Ensure, equals } from '@serenity-js/assertions';
import { Masked, Task } from '@serenity-js/core';
import { Navigate, Click, Enter, Text, PageElement, By } from '@serenity-js/web';
describe('Swag Labs', () => {
it('should let a standard user complete checkout', async ({ actor }) => {
await actor.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.
End-to-end testing:
| Stack | Live Report |
|---|---|
| Playwright Test | Serenity BDD ยท Playwright |
| Cucumber + Playwright | Serenity BDD |
Component testing:
| Stack | Live Report |
|---|---|
| Playwright CT + React | Serenity BDD ยท Playwright |
| Playwright CT + Lit/Web Components | Serenity BDD ยท Playwright |
FAQโ
Does it slow down my tests?โ
No. Serenity/JS adds negligible overhead. Tests still run on Playwright's engine with the same parallelism and auto-waiting.
Do I have to rewrite all my tests?โ
No. You can mix Screenplay and non-Screenplay tests in the same project. Start with reporting-only and introduce Screenplay gradually.
Does it work with UI Mode?โ
Yes. Serenity/JS integrates with Playwright UI Mode and augments the Trace Viewer with Screenplay activity information.
What about built-in fixtures?โ
Serenity/JS extends Playwright Test fixtures โ it doesn't replace them. You still have page, context, browser, and all your custom fixtures. Serenity/JS adds actor, actorCalled, and other Screenplay fixtures.
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 or Playwright's built-in HTML report.
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
- Installation guide โ detailed setup instructions
- Project templates โ all available starter projects
- Troubleshooting โ solutions to common issues
Serenity/JS works just as well with WebdriverIO. The Task code is identical โ only the config differs. See Serenity/JS with WebdriverIO.