Skip to main content

Why Serenity/JS

Your test suite should be more than a collection of scripts that pass or fail. It should be shared evidence of what your system does, how reliably it does it, and whether it's ready to ship — evidence that developers, testers, and business stakeholders can all read and act on.

That's what Serenity/JS helps you build.

Serenity/JS HTML Report — shared evidence your whole team can act on (source)

Tests and reports that explain your system

Most test reports list what's broken. Serenity/JS reports explain what your system does — and how confidently you can ship it:

  • For developers: activity trees show the exact step that failed, with screenshots, HTTP exchanges, and timing — reducing diagnosis from minutes to seconds
  • For QA engineers: consistency tracking reveals which tests are genuinely broken vs. flaky vs. recovering — so you invest effort where it matters
  • For product owners and managers: the dashboard answers "can we ship?" with a confidence score, pass rate, and trend chart — no technical translation needed
  • For the whole team: the Capabilities view maps test results to your directory structure, turning your spec files into navigable living documentation of what the system does

The Serenity/JS HTML Reporter produces these reports automatically from your test results. But a report is only as meaningful as the tests behind it — which is where the Serenity/JS framework comes in.

See a live report | HTML Reporter guide


Tests expressed in domain language

For reports to serve the whole team, tests need to describe what the actors in your system are doing and why — how a customer completes the checkout flow, how a third-party integrates with your APIs, how an administrator audits the system — not just how browsers click buttons or REST clients send requests.

This requires tests to be written using the vocabulary of the business workflows they represent, with implementation details encapsulated so they don't pollute the test. As described in BDD in Action, Second Edition, this separation naturally organises into three layers — and Serenity/JS gives you the building blocks to implement them:

LayerConcernSerenity/JS concept
SpecificationWhat the system should doTest scenarios (describe, it)
DomainHow actors accomplish goals — composable workflows in business languageActors + Tasks
IntegrationWhere the system is interacted with — web UIs, APIs, mobile appsAbilities + Interactions + Questions

Each layer depends only on the layer below it, so when something changes, the impact stays contained:

  • UI redesign — update selectors in the Integration layer; your business workflows and test scenarios don't change
  • New business rule — add scenarios and Tasks; the integration code stays the same
  • Switch Selenium → Playwright — swap the configuration; your Tasks and specs are untouched
  • Change integration APIs — update the Integration layer; nothing above it notices

→ Learn more: Screenplay Pattern | Serenity/JS Architecture | BDD in Action, Second Edition


The Screenplay Pattern makes this practical

The Screenplay Pattern is the architectural mechanism that makes domain-language tests possible without ceremony or heavyweight abstractions:

  • Actors represent people and external systems interacting with the system under test
  • Abilities are thin wrappers around integration libraries needed to interact with your system's interfaces
  • Tasks model sequences of activities as meaningful steps of a business workflow
  • Interactions represent the low-level activities an actor can perform using a given interface
  • Questions retrieve information from the system under test and the test execution environment

The same Tasks and Questions work across Playwright, WebdriverIO, Cucumber, and other supported test runners — because they depend on abstractions, not on any specific integration library.

Works with multiple tools

The examples below use Playwright Test, but Screenplay Pattern code is identical regardless of what test runner or browser driver you use — that's the point of the architecture.


Adopt at your own pace

Serenity/JS draws on over two decades of the author's experience helping teams of all sizes — from startups shipping MVP webapps to multinationals building online banking systems and sophisticated trading platforms. We know that no team can stop delivering to rearchitect their test suite. That's why Serenity/JS supports gradual adoption: start with just reporting, introduce Screenplay interactions where they add value, and let new and legacy tests run side by side — no migration deadline required.

To show you how this works in practice, here's the same test at four levels of adoption, from vanilla Playwright through to the full Screenplay Pattern.

Scenario: Existing Swag Labs customer successfully orders an item

  • Customer logs in
  • Adds a backpack to their cart
  • Successfully completes checkout
About Swag Labs

Swag Labs is a demo e-commerce website created by Sauce Labs specifically for practicing test automation. It's a fictive online store where you can browse products, add items to a cart, and complete checkout flows.

Level 0: Where most teams start

No Serenity/JS — all logic in flat test scripts. This is where most teams start — and where most test suites stay until the pain of duplication and opaque failures forces a change.

tests/checkout.spec.ts
import { test, expect } from '@playwright/test';

test('should complete checkout successfully', async ({ page }) => {
await page.goto('https://www.saucedemo.com/');
await page.locator('[data-test="username"]').fill('standard_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();

await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();

await page.locator('[data-test="shopping-cart-link"]').click();
await page.locator('[data-test="checkout"]').click();

await page.locator('[data-test="firstName"]').fill('Alice');
await page.locator('[data-test="lastName"]').fill('Smith');
await page.locator('[data-test="postalCode"]').fill('90210');
await page.locator('[data-test="continue"]').click();

await page.locator('[data-test="finish"]').click();
await expect(page.locator('[data-test="complete-header"]'))
.toHaveText('Thank you for your order!');
});

What you get: A working test. Quick to write when you have a handful of scenarios.

What breaks down at scale:

  • Selector duplication — when [data-test="login-button"] becomes [data-test="sign-in"], you grep the codebase and update every file that references it. With 200 tests and 30 that need login, that's 30 edits for one rename.
  • Opaque failures — reports show "failed at locator [data-test="login-button"]". To understand what the test was verifying, you have to mentally parse the whole script.
  • No reuse — login, "add to cart", checkout — each sequence is copy-pasted into every test that needs it.
  • AI amplifies the problem — coding agents trained on your codebase reproduce the same copy-paste patterns, generating more tests with the same duplication and fragility, faster than you can review their changes.
Credentials in examples

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.


Level 1: Add Serenity/JS reporting

The smallest possible improvement: add the Serenity/JS HTML Reporter to your test runner configuration. Your test code stays completely unchanged, but the reports now show trend history across CI runs, flaky test detection, error clustering, and an interactive dashboard — all in a self-contained report you can open directly from your filesystem, or serve from GitHub Pages or GitLab Pages.

For example, with Playwright Test:

playwright.config.ts
  import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests',
- reporter: 'html',
+ reporter: [
+ [ 'html' ], // Keep the default Playwright HTML reporter for low-level debugging
+ [ '@serenity-js/playwright-test', {
+ crew: [
+ '@serenity-js/console-reporter',
+ [ '@serenity-js/html-reporter', {
+ specDirectory: './tests'
+ } ],
+ ]
+ }]
+ ],
// ... rest of your config stays the same
});

That's it — no separate report generation step, no additional scripts. The report is produced automatically at the end of the test run.

→ Learn more: HTML Reporter configuration | Configuring the test runner

What you gain:

  • Trend history — see how pass rate and duration change across CI runs; spot regressions immediately
  • Flaky test detection — tests automatically classified as flaky, degraded, inconsistent, or recovered
  • Error clustering — failures grouped by root cause, not listed individually
  • Interactive dashboard — confidence score, pass rate, completeness, and consistency at a glance
  • Zero changes to your test code — only the config is updated

What still breaks down at scale:

  • Selectors still duplicated — a UI refactor still means a multi-file find-and-replace
  • No code reuse — login logic is still copy-pasted into every test that needs an authenticated user
  • Diagnosis gap — reports tell you which test failed and cluster errors, but the test code itself still can't show you the step-by-step execution at a glance
Serenity/JS HTML Report — Dashboard with trend chart and KPI cards (source)

Set this up in 5 minutes


Level 2: Screenplay + Playwright hybrid

Now that you have reporting, the next step is making your tests produce better reports. Swap the import to @serenity-js/playwright-test to access the actor fixture, then introduce Screenplay interactions where they add clarity. Each interaction appears as a named, timed step in the report — and your existing tests continue to work unchanged alongside the new ones.

tests/checkout.spec.ts
import { test, expect } from '@serenity-js/playwright-test';  // ← swap the import
import { Navigate } from '@serenity-js/web';

test('should complete checkout successfully', async ({ actor, page }) => {
// Screenplay interaction — shows as a named step in reports
await actor.attemptsTo(
Navigate.to('https://www.saucedemo.com/'),
);

// Vanilla Playwright — still works, just won't appear as named steps
await page.locator('[data-test="username"]').fill('standard_user');
await page.locator('[data-test="password"]').fill('secret_sauce');
await page.locator('[data-test="login-button"]').click();

await page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click();
await page.locator('[data-test="shopping-cart-link"]').click();
await page.locator('[data-test="checkout"]').click();

await page.locator('[data-test="firstName"]').fill('Alice');
await page.locator('[data-test="lastName"]').fill('Smith');
await page.locator('[data-test="postalCode"]').fill('90210');
await page.locator('[data-test="continue"]').click();
await page.locator('[data-test="finish"]').click();

await expect(page.locator('[data-test="complete-header"]'))
.toHaveText('Thank you for your order!');
});

What you gain over Level 1:

  • Screenplay interactions appear as named steps in reports — with timing and screenshots
  • Access to actor, actorCalled, and other Serenity/JS fixtures
  • A library of ready-made interactions — Navigate, Click, Enter, and many more
  • Incremental adoption — convert interactions one at a time without rewriting entire tests

What still breaks down at scale:

  • Sequences still inlined — two tests that both need "add item to cart" each spell out the same clicks and selectors
  • No single source of truth — selectors live in test files rather than in a shared location, so a rename still fans out
  • Limited composability — named steps improve reports, but cross-test reuse requires the next step: factoring common workflows into composable Tasks

Level 3: Full Screenplay Pattern

This is where the architecture pays off in full. Tests describe what the actors do in business language — and the report reflects that directly. Selectors, page interactions, and API calls live in separate, reusable classes that any test can compose. Compare the spec file below with the flat script from Level 0:

tests/swag-labs.spec.ts
import { describe, it } from '@serenity-js/playwright-test';
import { Ensure, equals } from '@serenity-js/assertions';
import { Navigate } from '@serenity-js/web';

import { Authenticate } from '../screenplay/Authenticate';
import { Inventory } from '../screenplay/Inventory';
import { Checkout } from '../screenplay/Checkout';

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!')),
);
});
});

What you gain over Levels 1–2:

  • Readable tests — the scenario reads like a user story, not a browser script
  • Reusable building blocksAuthenticate.withCredentials() works in every test; change a selector once, fix every test that needs it
  • Rich activity breakdown — reports decompose high-level Tasks into their constituent steps, each with timing and evidence, so you can trace exactly where a workflow succeeded or failed
  • Multi-actor support — buyer + seller, admin + user scenarios become trivial to implement
  • Blended testing — use APIs for fast test data setup, UI only where it matters
  • Tool independence — swap Playwright for WebdriverIO, or use both in the same project, without touching test logic
Tasks are portable

The Task code imports from @serenity-js/web and @serenity-js/core — not from Playwright or WebdriverIO. Switch tools (or use both) without rewriting your Domain layer. Only the Ability configuration changes.

Try the Screenplay Pattern | Full Playwright tutorial | WebdriverIO tutorial


Ready to start?

Pick the guide for your test runner — each one takes you from zero to a working report in under 5 minutes:

What others are saying