Skip to main content

Your first web scenario

In this guide, you'll learn the basics of web testing with Serenity/JS; I'll show you how to run and debug existing tests and write new test scenarios. While we'll focus on helping you get the basics right, I'll point you to the more advanced techniques and patterns when needed.

My goal with all the Serenity/JS guides in this handbook, including this one, is that they're easy to follow whether you're a test automation expert or just starting on your journey. If you found anything here that could have been clearer, please let me know in the comments or submit a correction.

To keep things simple, we'll use a Gitpod.io workspace to work with Serenity/JS in your web browser, so there's no need to install anything on your computer. If you prefer to set up Serenity/JS locally instead, follow the installation instructions in Serenity/JS + Playwright Test project template.

Open in Gitpod

In this tutorial, you'll learn that:

  • Serenity/JS works well with popular development environments, including the free Visual Studio Code,
  • Serenity/JS tests are just high-quality code, so all your regular programming tools will work as expected,
  • Serenity/JS test scenarios are actor-centric and follow the Screenplay Pattern to help you capture business domain vocabulary and business workflows,
  • Serenity/JS uses an activity-based composition model, optimised for readability and code reuse,
  • Serenity/JS assertions and synchronisation statements are portable across interfaces and integration tools,
  • Serenity/JS has first-class support for TypeScript and the asynchronous nature of the JavaScript runtime.
Pro Tip

If you get lost or stumble upon a problem you're not quite sure how to solve - ask on the Serenity/JS Community Chat.

Launching your workspace​

All Serenity/JS project templates, such as the one we'll use in this chapter, support Gitpod.io workspaces and are configured to make it easy for you to use them in a Visual Studio Code-based development environment. Of course, since Serenity/JS tests are standards-based Node.js code, they'll work just as well in any other modern IDE.

In this tutorial, we'll use Serenity/JS + Playwright Test template, which integrates Serenity/JS with Playwright web testing library and its dedicated Playwright Test test runner. The test suite we'll work on interacts with a simple to-do list app that you can experiment with at todo-app.serenity-js.org.

To launch your workspace, make sure you have a GitHub account, which will make it easier for you to use all the other Serenity/JS resources too.

Next, launch your Gitpod.io web IDE using the "Open in Gitpod" button below and sign in to your Gitpod.io workspace using your GitHub account.

Open in Gitpod

Did you know?

Serenity/JS integrates with several popular test runners, such as Cucumber, Mocha, Jasmine and Playwright Test, as well as various web integration tools, such as Selenium, Playwright and WebdriverIO. Once you know your way around your first Serenity/JS project template, picking the one right for your team and your project will become a breeze.

Running tests in Visual Studio Code​

Serenity/JS + Playwright Test template you've just opened in your Gitpod workspace includes several example test scenarios located under the spec directory.

You can view them using your Visual Studio Code Project Explorer sidebar, which should show a directory structure similar to this:

Visual Studio Code user interface showing an example test scenario

There are two ways to run Playwright tests in Visual Studio Code, and you can do it either by:

  • using the play/check mark icon next to the name of the test in a .spec.ts file,
  • using the play icon in the Visual Studio Code Test Explorer panel, which shows all the test scenarios Visual Studio Code has detected in your project, as per the screenshot below:
Running test scenarios in Visual Studio Code
Running test scenarios in Visual Studio Code

Once you've inspected the spec/recording-items.spec.ts, you will notice that we're using two functions provided by the @serenity-js/playwright-test module that help to organise a test suite:

  • it - to declare a single test scenario
  • describe - to declare a group of test scenarios

Those describe and it functions are wrappers around Playwright Test test.describe and test functions, respectively.

In addition to the functionality offered by Playwright test function, Serenity/JS it wrapper offers Serenity/JS-specific test fixtures, such as actor, actorCalled, or crew. I'll tell you about them in a moment, but first, let's run the scenarios.

Pro Tip

If Visual Studio Code didn't detect any test scenarios in your project, or if it doesn't display the play/check mark icon next to the name of the test, use the "Refresh Tests" icon in the Test Explorer to reload test configuration.

Visual Studio Code Test Explorer panel showing the "Refresh tests" button
Visual Studio Code Test Explorer panel showing the "Refresh tests" button

Exercises​

To get familiar with running Serenity/JS test scenarios in Visual Studio Code, conduct the following experiments:

  1. Open spec/recording-items.spec.ts and run test scenarios such as it('should clear text input field when an item is added') individually using the play/check mark icon next to the it block. Make sure they're passing.
  2. Run a group of test scenarios by clicking on the play/check mark icon next to the describe block, such as describe('Todo List App'). Make sure they're passing.
  3. Run individual test scenarios using Visual Studio Code Test Explorer panel.
  4. Navigate from a test scenario in a Visual Studio Code Test Explorer panel to its location in the codebase using the "Go to test" icon next to the name of the test.

Writing Serenity/JS test scenarios​

Now that you know how to run Serenity/JS test scenarios in VS Code, let's talk about what's involved in writing some new ones.

First, add the below code snippet to your existing test suite in spec/recording-items.spec.ts by placing it inside the describe('Todo List App') block.

spec/recording-items.spec.ts
describe('Todo List App', () => {

it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
startWithAnEmptyList(),

recordItem('Buy some milk'),

Ensure.that(itemNames(), equals([
'Buy some milk',
])),
)
})

// other test scenarios
})

Well done, you've just created your first Serenity/JS test scenario!

Make sure that you can run this new scenario and that it's passing, and we'll analyse its structure line by line in just a moment.

Adding a new Serenity/JS test scenario using Visual Studio Code on Gitpod.io
Adding a new Serenity/JS test scenario using Visual Studio Code on Gitpod.io

As you can see in the listing above, well-written Serenity/JS test scenarios are concise, easy to read, and easy to understand even to audiences who might not necessarily have background in technology. That's because Serenity/JS is designed to help you express your scenarios in the domain language of your company and your business and avoid incidental detail and low-level implementation noise that could cloud the picture.

Showing the browser window​

So far you've been running your Serenity/JS test scenarios, but didn't get a chance to see the actual browser interacting with the web app under test. That's because any web browsers used by Serenity/JS project templates are configured to run in "headless" mode by default so that they don't get in the way and don't interrupt your work.

This setting is configurable, so you can change it when you need to see what's going on under the hood or to access the developer tools offered by your browser to debug your test scenarios or the system under test.

In addition to being able to interact with the browser when running the tests locally, the Serenity/JS + Playwright Test GitPod.io workspace I have prepared for you comes equipped with VNC, so you can see the browser on Gitpod.io as well.

To see the browser:

  • Enable "Show browser" checkbox in the Playwright panel
  • In the "Ports" panel, open VNC running on port 6080 in "Preview" mode (this step applies only to Gitpod.io)
    • If your "Ports" panel is empty, click the bell in the lower right corner and click the appropriate "Open preview" button.
  • Run the test!
Viewing the browser window while running Serenity/JS test scenarios in Visual Studio Code on Gitpod.io
Viewing the browser window while running Serenity/JS test scenarios in Visual Studio Code on Gitpod.io

If your development environment is not based on Visual Studio Code, or if your version of Playwright Test Extension for VSCode displays a warning message saying Show browser mode does not work in remote vscode, you can configure your tests to start the browser in non-headless mode by setting the headless flag to false in Playwright Test configuration file:

playwright.config.ts
import type { PlaywrightTestConfig } from '@serenity-js/playwright-test'
const config: PlaywrightTestConfig = {
use: {
headless: false,
// ...
},
}

export default config

Now that you know how to run the tests, let's get back to code.

Designing actor-centred test scenarios​

You might have noticed that the scenario you've just added starts with an actor. In fact, all Serenity/JS test scenarios are actor-centred and start like that. This design, based on the Screenplay Pattern, helps your test scenarios move away from automation and integration tools taking centre stage and instead helps you focus on actorsβ€”people and processes interacting with the system under test.

So what are actors? Actors can represent end-users, like a shopper interacting with an online store, or a traveller interacting with a flight booking system. However, actors can also represent automated processes and external systems acting upon the system under test. Examples here could include an external financial data provider submitting asset price information to our batch processing system, or a website crawler bot scanning a web UI and receiving a different version than a regular user. You can also use actors to represent components of a software system initiating some interaction with the component we're interested in, like a microservice sending requests to another microservice under test.

Remember

Actors represent people and external systems interacting with the system under test.

From the implementation perspective, the Serenity/JS Playwright Test module takes care of initialising and injecting actors into your test scenarios and dismissing them and freeing their resources when the test scenario is finished.

All you need to do is to use the Serenity/JS it function instead of the default Playwright test function, and ask it for an actor:

spec/recording-items.spec.ts
// Use Serenity/JS `describe` and `it` wrappers from `@serenity-js/playwright-test` module:
import { describe, it } from '@serenity-js/playwright-test'

// `describe` groups test scenarios together
describe('Todo List App', () => {

// `it` injects the `actor` object when a test scenario requests it
it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
// ...
)
})

// ...
})

Serenity/JS test scenarios can involve one or multiple actors. This is useful, for example, when designing scenarios for contexts where several actors interact with one another, or where several actors are required to complete different parts of a larger workflow. Examples of such contexts include multi-user workflow systems, messaging systems, video games, and so on. You can see an example of a basic multi-actor test scenario in spec/multi-actor.spec.ts.

Modelling workflows using reusable activities​

The role of Serenity/JS actors is to interact with the system under test by performing activities.

You use activities to model the logical elements of a workflow that an end-user or an external system interacting with the system under test would perform to accomplish some goal.

Remember

Actors perform activities to interact with the system under test and accomplish their goals.

From the implementation perspective, Serenity/JS activities are the most basic form of code reuse and manifest themselves as either tasks or interactions. We'll discuss both of those in detail when we talk about the Screenplay Pattern, but for now let's use the following definitions:

  • Interactions are the low-level activities that interact directly with an interface of the system under test. Examples here include an interaction to click on a button in a web UI, or one to send a POST request to a REST API. Serenity/JS provides dozens of such interactions out of the box, and you can create your own when needed.
  • Tasks are composites of other activities, created to capture business domain vocabulary and help to make steps in your test scenario reflect steps in your business workflow. Examples here might include a task to "record a to-do item", "create a customer account", or to "purchase a plane ticket".

In our example, a task that accomplishes the goal of "record a to-do item" can be achieved by performing three lower-level activities:

  • enter the name of the to-do item into the "What needs to be done?" input box,
  • press the enter key,
  • make sure the item is recorded as expected.

You can try to perform this task yourself at todo-app.serenity-js.org:

Demonstration of recording a to-do item
Demonstration of recording a to-do item

To see how a task like that would be implemented with Serenity/JS, open spec/todo-list-app/TodoItem/tasks.ts and review the implementation of a task to recordItem:

spec/todo-list-app/TodoItem/tasks.ts
import { contain } from '@serenity-js/assertions'
import { Task, Wait } from '@serenity-js/core'
import { Enter, Key, Press } from '@serenity-js/web'

import { newTodoInput } from '../TodoApp'
import { itemNames } from '../TodoList'

export const recordItem = (name: string): Task => // 1 - reusable function
Task.where(`#actor records an item called ${ name }`, // 2 - task description
Enter.theValue(name).into(newTodoInput()), // 3 - sequence of lower-level activities
Press.the(Key.Enter).in(newTodoInput()),
Wait.until(itemNames(), contain(name)),
)

In the listing above, you can see that a definition of a Serenity/JS task is compact, easy to read, and easy to understand. In fact, it reads almost exactly like how I explained the task to you earlier. This design is intentional and makes your code much more accessible and easier to reason about.

To implement custom tasks like this and capture the language of your business domain all you need is just a few lines of code where you define:

  1. a reusable function, named after the goal an actor will accomplish having performed a given task, e.g. recordItem,
  2. a task description, used for test reporting purposes,
  3. a sequence of lower-level activities that constitute a task.

What's important to reiterate here is that Serenity/JS activities are designed to be reusable. This means you could write a task like the one to recordItem just once and reuse it in any scenario that needs to record to-do items. If the steps involved in how an actor would go about recording a to-do item in your system were to change, you'd only have to update them in this one place in your codebase where the task is defined and not in every single scenario that uses it.

Did you know?

The task decomposition model implemented by Serenity/JS is based on Hierarchical Task Analysis, a technique typically used in User-Centred Design. You can read more about it in my article "User-Centred Design: How a 50-year-old technique became the key to scalable test automation".

You can define as many or as few tasks as it makes sense to reflect the workflows in your domain. You can make your tasks as high-level or as low-level as you wish. You're not limited to interacting with just the web interfaces either! In fact, you can make your Serenity/JS actors interact with any interface of your system under test, be it web UIs, REST APIs, mobile apps, or whatever a Node.js program can talk to.

To make your work easier, Serenity/JS modules provide dozens of lower-level interactions you can compose your custom tasks from.

This will help you focus on modelling your business workflows and designing your test scenarios instead of wasting your time figuring out how to integrate with the given interface or trying to make several incompatible libraries work together.

The Serenity/JS activity-based code reuse model also means that as you evolve the vocabulary of your domain-specific tasks over time, writing test scenarios becomes easier as you're simply re-arranging existing tasks into new scenarios.

Using portable assertions​

The Serenity/JS activity-based programming model applies to performing assertions just as well as it applies to performing tasks and interactions. In fact, the Serenity/JS assertion to Ensure is just another interaction you give to an actor to perform.

Consider the test scenario you wrote earlier:

spec/recording-items.spec.ts
describe('Todo List App', () => {

it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
startWithAnEmptyList(),

recordItem('Buy some milk'),

Ensure.that(itemNames(), equals([
'Buy some milk',
])),
)
})
})

The last activity in the sequence above is an assertion for the actor to Ensure.that the names of the items in our to-do list meet our expectation:

import { Ensure, equals } from '@serenity-js/assertions'

await actor.attemptsTo(
// ...
Ensure.that(itemNames(), equals([ 'Buy some milk', ])),
// ^ actual ^ expectation
)

All Serenity/JS assertions follow the same consistent pattern and all the Serenity/JS expectations (such as equals) are interface-agnostic. This design helps to make your test code portable across interfaces and lower-level integration tools and enables you to use a single pattern no matter the type of interface you interact with.

In addition to interface-agnostic expectations, Serenity/JS also ships with web-specific expectations like isVisible() that are portable across web integration tools, such as Playwright, WebdriverIO, or Selenium.

Analysing assertion failures​

You've already seen what happens when an assertion passes. But what happens when it fails?

To experience how you'd go about analysing a Serenity/JS assertion failure, modify the scenario you wrote earlier to make it fail:

spec/recording-items.spec.ts
describe('Todo List App', () => {

it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
startWithAnEmptyList(),

recordItem('Buy some cake'),

Ensure.that(itemNames(), equals([
'Buy some milk',
])),
)
})
})

When you run the scenario again, you'll notice how the advanced support for Visual Studio Code with Playwright Test extension, enabled by Serenity/JS Playwright Test module, makes it easy to see exactly where the assertion failure has happened.

Advanced integration between Serenity/JS and Visual Studio Code with Playwright Test extension
Advanced integration between Serenity/JS and Visual Studio Code with Playwright Test extension

Exercises​

  1. Compare a single-actor scenario in spec/recording-items.spec.ts and a multi-actor scenario in spec/multi-actor.spec.ts. What differences and what similarities can you see in how the actors are accessed?
  2. Inspect tasks in spec/todo-list-app/TodoItem/tasks.ts. What Serenity/JS web interactions do they use? Can you find their usage examples in the API docs of the @serenity-js/web module?
  3. Write another test that adds two items to the to-do list and verifies they've been added correctly. What task can you reuse?
  4. Add another interaction to your new test - an assertion that verifies the number of items in the list: Ensure.that(itemNames().length, equals(2))
  5. See if you can create a test that adds 2 items, removes one of them, and then verifies the items that are left. Reuse the existing task to remove(itemName).
  6. Compare the activity-based code reuse model used by Serenity/JS with other code reuse models you've seen used in the context of test automation in the past. How do they compare?

Debugging tests and interactive execution​

A standard Node.js debugger used by your IDE is designed to deal with basic, imperative code, and not the declarative programming model used by Serenity/JS. This means that using a Visual Studio Code debugger to set a breakpoint on one of the activities given to an actor to perform will not have the desired effect. That's because your IDE pauses the scenario when the activity is declared and not when it's executed.

Luckily, Serenity/JS has a solution to this problem.

Visual Studio Code debugger pauses execution when the activity is declared, not when it is executed
Visual Studio Code debugger pauses execution when the activity is declared, not when it is executed

Debugging test scenarios​

To help a Node.js debugger bridge the gap between the imperative and declarative programming models, Serenity/JS offers debugging capabilities and the interaction to Debug. To use it, add the following code snippet to your test scenario, making sure to also use Visual Studio Code Quick Fix feature to add import { Debug } from '@serenity-js/core':

Debug.values(() => {
// set breakpoint
}),

Next, set a breakpoint on the line indicated by the comment, and run your test scenario in debug mode to see your IDE pause the execution between actor's activities:

Visual Studio Code debugger pauses execution when the interaction to Debug is invoked
Visual Studio Code debugger pauses execution when the interaction to Debug is invoked

Debugging tasks​

The great thing about the interaction to Debug is that you can use it not just in the top-level test scenarios, but also in any custom Serenity/JS tasks you define.

To try it out, open spec/todo-list-app/TodoApp/tasks.ts and modify the task called startWithAnEmptyList by adding the interaction to Debug and set a breakpoint on the line indicated by the comment:

spec/todo-list-app/TodoApp/tasks.ts
import { Ensure, equals } from '@serenity-js/assertions';
import { Task, Debug } from '@serenity-js/core';
import { Navigate, Page } from '@serenity-js/web';

export const startWithAnEmptyList = () =>
Task.where(`#actor starts with an empty todo list`,
Navigate.to('/'),
Ensure.that(
Page.current().title().describedAs('website title'),
equals('Serenity/JS TodoApp'),
),
Debug.values(() => {
// set breakpoint
})
);

// ...

When you run a scenario using the task to startWithAnEmptyList in debug mode, you'll notice that the execution pauses where you wanted it to pause. However, setting breakpoints is not the only thing the interaction to Debug can do.

Retrieving information using questions​

In the Screenplay Pattern, we design our automated tests to model how actors perform their activities to accomplish their goals. However, performing interactions is just one side of the coin. The other thing an actor must do is to determine if the behaviour of the system under test meets its acceptance criteria. To do that, actors need to retrieve information from the system, its execution environment and sometimes additional data sources.

Serenity/JS models the act of information retrieval as questionsβ€”an actor answers a question to obtain some information. Since questions represent a way for actor to obtain the information, and not the result itself, they can be used to parameterise actor's activities and resolved whenever the actor gets to performing the parameterised activity.

Remember

Actors answer questions to obtain information.

To see examples of how to define Serenity/JS questions, open spec/todo-list-app/TodoList/questions.ts.

Among several other functions you'll spot itemNames(), which should be already familiar to you:

spec/todo-list-app/TodoList/questions.ts
import { includes } from '@serenity-js/assertions';
import { Answerable, d, QuestionAdapter } from '@serenity-js/core';
import { By, PageElement, PageElements, Text } from '@serenity-js/web';

export const items = () =>
PageElements.located(By.css('.todo-list li'))
.describedAs('displayed items');

export const itemNames = () =>
Text.ofAll(items())
.map(name => name.trim())
.describedAs('displayed items') as QuestionAdapter<string[]>;

export const itemCalled = (name: Answerable<string>) =>
items()
.where(Text, includes(name))
.first()
.describedAs(d`an item called ${ name }`) as QuestionAdapter<PageElement>;

Just like Serenity/JS activities can be composed into tasks, Serenity/JS questions can be composed with other questions to transform their results.

Remember

You can compose questions to transform their results.

In particular:

  • the question about items() resolves to page elements corresponding to HTML widgets describing entries in the todo-list,
  • the question about itemNames() retrieves the text content of those elements, and applies a simple transformation to trim space characters from both ends of the value,
  • the question about itemCalled(name) uses Serenity/JS Page Element Query Language to retrieve a single item that matches an expectation,

Also note how all the questions define custom descriptions for Serenity/JS to use when reporting.

Debugging questions​

To use the interaction to Debug to analyse what a given question resolves to, modify the scenario you wrote earlier:

spec/recording-items.spec.ts
describe('Todo List App', () => {

it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
startWithAnEmptyList(),

recordItem('Buy some milk'),

Ensure.that(itemNames(), equals([
'Buy some milk',
])),

Debug.values(
(results, names) => {
// set breakpoint here
},
// one or more questions to analyse
itemNames()
)
)
})

// other test scenarios
})

When you run the scenario in debug mode and the execution pauses on the line where you've set the breakpoint, you'll notice that the actor has resolved the question returned by itemNames() and that the Run and Debug panel shows the values of results and names which you specified as your function arguments.

To learn more about how this works, check out the debugging guide.

Debugging the results of Serenity/JS questions
Debugging the results of Serenity/JS questions

Exploring page element locators​

The Serenity/JS Playwright Test module enables you to leverage the locator tuning capabilities of the Playwright Test Visual Studio Code extension. This functionality is provided by Playwright, and more specifically the page.locator API that it offers.

To use it, you'll need to:

  • use the interaction to Debug to pause the scenario at a breakpoint,
  • use PlaywrightPage.current().nativePage() from @serenity-js/playwright to extract a Playwright page from the Serenity/JS Page that's wrapping it,
  • interact directly with the page.locator API.

To see how the locator tuning features work in practice, modify the scenario you wrote earlier:

spec/recording-items.spec.ts
describe('Todo List App', () => {

it('should allow me to add a todo item', async ({ actor }) => {
await actor.attemptsTo(
startWithAnEmptyList(),

recordItem('Buy some milk'),

Ensure.that(itemNames(), equals([
'Buy some milk',
])),

Debug.values(
(results, page) => {
// set breakpoint on the line below
page.locator('h1')
},
PlaywrightPage.current().nativePage(),
)
)
})

// other test scenarios
})

When you run your scenario through a debugger and the execution pauses on the line where you set the breakpoint, you'll notice that you can modify the selector passed to page.locator and that your browser highlights the matching elements live as you do it.

Debugging the results of Serenity/JS questions
Debugging the results of Serenity/JS questions

Exercises​

To get familiar with debugging Serenity/JS test scenarios in Visual Studio Code, conduct the following experiments:

  1. Use the interaction to Debug to inspect the value of Page.current().title() and Page.current().url()
  2. Write a scenario that adds an item called "walk the dog" to the to-do list, and use the interaction to Debug to inspect the value returned by itemCalled('walk the dog').text()
  3. Explore other methods offered by Serenity/JS PageElement returned by itemCalled(name). How would you check if an element is clickable or visible?
  4. Read the guide on Serenity/JS debugging and experiment with live evaluation of locators like h1, footer p, or li.todo

Test reporting​

Serenity frameworks are well-known for their state-of-the-art reporting capabilities. Apart from the excellent Serenity BDD reports, however, Serenity/JS can also dramatically improve the standard Playwright Test reports. Reports produced for Playwright Test scenarios that follow the Screenplay Pattern automatically include detailed information about actors' activities and much more thorough assertion error reports thanks to the dedicated Serenity/JS assertions library.

The Serenity/JS + Playwright Test project template you've been using in this tutorial is already set up to produce both the Serenity BDD and Playwright Test reports.

To explore them, run the full test suite by invoking npm test in your terminal:

Running the full test suite in Visual Studio Code
Running the full test suite in Visual Studio Code

When the test suite is finished, you'll find your Serenity BDD reports generated under target/site/serenity, and served on port 8080 on Gitpod:

Serenity BDD reports
Serenity BDD reports

Playwright Test reports are generated under playwright-report and served on port 8181 on Gitpod:

Enhanced Playwright Test reports
Enhanced Playwright Test reports

Next steps​

Congratulations! πŸ₯³ You've just learnt how to write, run, and debug your first Serenity/JS test scenarios! If you enjoyed this tutorial, please leave a πŸ‘ in the reactions section below.

If you'd like to learn more, I'd suggest for you to:

New tutorials and videos are coming soon, follow Serenity/JS on LinkedIn and subscribe to Serenity/JS YouTube channel to get notified when they're available!

LinkedIn Follow YouTube Follow

Help us help you

Serenity/JS is a free open-source framework, so we rely on our wonderful GitHub sponsors to keep the lights on.

If you appreciate all the effort that goes into making sophisticated tools easy to work with, please support our work and become a Serenity/JS GitHub Sponsor today!

GitHub Sponsors