Continuing my series about Serenity/JS:
Dive into the potent pairing of Serenity/JS and Playwright.
Witness how this winning combination revolutionizes component testing.
Ready to elevate your testing game?
Playwright introduces an
experimental component testing approach.
In this tutorial, we'll explore how to seamlessly integrate component testing into a
React/Next.js application using Playwright in conjunction with
Serenity/JS, a powerful testing framework.

This tutorial provides a step-by-step guide to set up and execute tests from scratch.
However, if you prefer to skip these steps, you can access the complete example code in
this GitHub repository.
Setting Up an Example Application
Before diving into testing, let's establish a baseline React application.
Following the recommended guidelines,
we'll leverage Next.js as our framework of choice.
We'll initialize a Next.js application in
non-interactive mode,
incorporating TypeScript and Tailwind CSS.
npx create-next-app@latest next-sjs-ct-tutorial --ts --tailwind --eslint --app --src-dir --import-alias "@/*"
cd next-sjs-ct-tutorial
npm run dev[def]
Once completed, your newly created Next.js application will be
accessible at http://localhost:3000/.
Ensure that you've installed at least Node.js version 18.17.0 for Next.js,
the minimum required version by the time of writing.
You can manage Node.js versions using tools like Volta.
For example, pinning the Node.js version to the latest available, currently 20.11.1,
can be achieved with:
volta pin node@latest
git add .
git commit -m "Pinned Node.js version using Volta"
Now, let's enhance our basic page by creating a small application with the following
user story:
As a user, I want to interact with a counter component that displays a count value and allows me to increase or decrease it within a specified range.
The component should feature two buttons: "Increase" and "Decrease."
When I click the "Increase" button, the displayed count value should increment, providing immediate visual feedback. Conversely, clicking the "Decrease" button should decrement the count value. However, these actions should be constrained within a defined range, preventing the count from exceeding a maximum of 10 and a minimum value of 0.
To ensure intuitive interaction, the buttons should visually indicate when they are disabled, preventing further increment or decrement operations beyond the defined range. This clear visual feedback helps me understand the available actions at a glance, enhancing my overall user experience with the counter component.
Create a ./src/components/Counter.tsx
file within your project to implement this
functionality.
./src/component/Counter.tsx
'use client'
import { useState } from "react"
export const Counter = () => {
const [count, setCount] = useState(0)
const minCount = 0
const maxCount = 10
function onIncreaseButtonClick(): void {
if (count < maxCount) {
setCount(count + 1)
}
}
function onDecreaseButtonClick(): void {
if (count > minCount) {
setCount(count - 1)
}
}
const buttonStyle = "bg-blue-500 text-white font-bold py-2 px-4 rounded"
const hoverStyle = "hover:bg-blue-700"
const disabledButtonStyle = "bg-gray-400 cursor-not-allowed"
return (
<>
{}
<div className="flex space-x-2">
<div>Count:</div>
<div
data-testid="count">
{count}
</div>
</div>
{}
<div className="flex space-x-4">
{}
<button
className={`${buttonStyle} ${count >= maxCount ? disabledButtonStyle : hoverStyle}`}
onClick={onIncreaseButtonClick}
disabled={count === maxCount}
data-testid="button-increase">
Increase
</button>
{}
<button
className={`${buttonStyle} ${count === minCount ? disabledButtonStyle : hoverStyle}`}
onClick={onDecreaseButtonClick}
disabled={count === minCount }
data-testid="button-decrease">
Decrease
</button>
</div>
</>
)
}
Update ./src/app/page.tsx
to include the Counter component on the main page:
./src/app/page.tsx
import { Counter } from "@/components/Counter";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center p-24 space-y-4">
<Counter />
</main>
);
}
With these changes, you can now utilize the increase/decrease counter
functionality on your Next.js page.
git add .
git commit -m "Implemented Counter"
Integrating Playwright and Serenity/JS
Now, let's integrate the necessary dependencies for testing,
including Playwright and Serenity/JS, along with associated packages
for assertions and reporting.
npm install --save-dev \
@serenity-js/assertions \
@serenity-js/console-reporter \
@serenity-js/core \
@serenity-js/playwright \
@serenity-js/playwright-test \
@serenity-js/serenity-bdd \
@serenity-js/web \
@playwright/experimental-ct-react \
rimraf \
npm-failsafe
Update the package.json
file with scripts for cleaning, executing tests,
and generating reports using Serenity/JS.
./package.json
[...]
"scripts": {
[...]
"clean": "rimraf dist target",
"serenity-bdd:update": "serenity-bdd update",
"test:ct": "failsafe clean serenity-bdd:update test:execute test:report",
"test:execute": "playwright test -c playwright-ct.config.ts",
"test:report": "serenity-bdd run --features ./src/components"
}
[
...]
Define the Playwright configuration file playwright-ct.config.ts
, which includes
settings for test directories, reporters, browser configurations, and other options
for running the tests.
./playwright-ct.config.ts
import { defineConfig, devices } from '@playwright/experimental-ct-react'
import { SerenityOptions } from '@serenity-js/playwright-test'
export default defineConfig<SerenityOptions>({
testDir: './src/components',
snapshotDir: './__snapshots__',
timeout: 10 * 1000,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
[ 'line' ],
[ 'html', { open: 'never' } ],
[ '@serenity-js/playwright-test', {
crew: [
[ '@serenity-js/serenity-bdd', { specDirectory: './src/components' } ],
'@serenity-js/console-reporter',
[ '@serenity-js/core:ArtifactArchiver', { outputDirectory: './target/site/serenity' } ],
],
} ],
],
use: {
trace: 'on-first-retry',
ctPort: 3100,
headless: true,
crew: [
[ '@serenity-js/web:Photographer', {
strategy: 'TakePhotosOfInteractions'
} ]
],
defaultActorName: 'Tess',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
})
In this code block, tasks and page elements for Serenity/JS
are defined to ensure the functionality of the counter component.
Tasks include checking button states, verifying count values,
and other assertions related to the counter component.
./src/components/CounterSerenitiy.ts
import { Ensure, contain, equals, not } from "@serenity-js/assertions";
import { Task } from "@serenity-js/core";
import { By, PageElement, isEnabled, Text, CssClasses } from "@serenity-js/web";
export class CounterSerenity {
static CounterLabel = PageElement
.located(By.css('[data-testid="count"]'))
.describedAs('label that shows the current count')
static IncreaseButton = PageElement
.located(By.css('[data-testid="button-increase"]'))
.describedAs('button to increase the count')
static DecreaseButton = PageElement
.located(By.css('[data-testid="button-decrease"]'))
.describedAs('button to decrease the count')
static EnsureIncreaseButtonIsEnabled = () =>
Task.where('#actor ensures that increase button is enabled',
Ensure.that(
CssClasses.of(this.IncreaseButton),
not(contain('cursor-not-allowed')))
)
static EnsureIncreaseButtonIsNotEnabled = () =>
Task.where('#actor ensures that increase button is not enabled',
Ensure.that(
CssClasses.of(this.IncreaseButton),
contain('cursor-not-allowed'))
)
static EnsureDecreaseButtonIsEnabled = () =>
Task.where('#actor ensures that decrease button is enabled',
Ensure.that(
CssClasses.of(this.DecreaseButton),
not(contain('cursor-not-allowed')))
)
static EnsureDecreaseButtonIsNotEnabled = () =>
Ensure.that(
CssClasses.of(this.DecreaseButton),
(contain('cursor-not-allowed'))
)
static EnsureCount = (count: string) =>
Task.where(`#actor ensures count label displays a value of ${count}`,
Ensure.that(Text.of(this.CounterLabel), equals(count)))
}
This section contains the test specifications for the Counter component using
Playwright and Serenity/JS. Tests cover scenarios such as initial state verificatio
and interactions to reach upper count limits while ensuring button states and count
values are correctly maintained.
./src/components/Counter.spec.tsx
import { test as componentTest } from '@playwright/experimental-ct-react'
import { useBase } from '@serenity-js/playwright-test'
import { Click, PageElement } from '@serenity-js/web'
import React from 'react'
import { Counter } from './Counter'
import { CounterSerenity } from './CounterSerenity'
import { Duration, Wait } from '@serenity-js/core'
const { it, describe } = useBase(componentTest)
describe('Counter', () => {
it('ensures initial state', async ({ mount, actor }) => {
const x = PageElement.from(await mount(
<Counter />
)).describedAs('counter component')
await actor.attemptsTo(
CounterSerenity.EnsureIncreaseButtonIsEnabled(),
CounterSerenity.EnsureDecreaseButtonIsNotEnabled(),
CounterSerenity.EnsureCount('0')
)
})
it('ensures state after click to upper limit', async ({ mount, actor }) => {
const x = PageElement.from(await mount(
<Counter />
)).describedAs('counter component')
await actor.attemptsTo(
CounterSerenity.EnsureIncreaseButtonIsEnabled(),
CounterSerenity.EnsureDecreaseButtonIsNotEnabled(),
CounterSerenity.EnsureCount('0'),
Click.on(CounterSerenity.IncreaseButton),
CounterSerenity.EnsureIncreaseButtonIsEnabled(),
CounterSerenity.EnsureDecreaseButtonIsEnabled(),
CounterSerenity.EnsureCount('1'),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
CounterSerenity.EnsureIncreaseButtonIsEnabled(),
CounterSerenity.EnsureDecreaseButtonIsEnabled(),
CounterSerenity.EnsureCount('6'),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
Click.on(CounterSerenity.IncreaseButton),
CounterSerenity.EnsureCount('10'),
CounterSerenity.EnsureIncreaseButtonIsNotEnabled(),
CounterSerenity.EnsureDecreaseButtonIsEnabled(),
)
})
})
Finally, include a basic HTML template index.html
for the testing page,
and create an entry point (index.tsx) for rendering React components.
./playwright/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="index.tsx"></script>
</body>
</html>
./playwright/index.tsx
import '../src/app/globals.css'
With everything set up, you can run the tests using npm run test:ct
,
and find the Serenity-BDD report under ./target/site/serenity
.
Before committing the latest changes, update your .gitignore
file:
.gitignore
/target/site
/playwright-report
/playwright/.cache
This concludes our tutorial on simplifying component testing with React/Next.js,
Playwright, and Serenity/JS. Happy testing!