Skip to main content

Debugging

Serenity/JS helps you model your test scenarios from the perspective of actors performing activities to accomplish their goals. Debugging follows this same consistent approach, with any debug statements expressed using the interaction to Debug.

The interaction to Debug accepts a debuggerFunction and any number of arguments, including dynamic values such as questions and question adapters:

import { actorCalled, Debug } from '@serenity-js/core'
import { Navigate, Page, PageElement, By } from '@serenity-js/web'

await actorCalled('Debbie').attemptsTo(
Navigate.to('https://serenity-js.org'),
Debug.values(
// debuggerFunction:
(results, url, tagline) => {
// set a breakpoint anywhere inside the function to view:
// - `results`, which evaluates to Array<DebuggingResult>
// - `url`, which evaluates to 'https://serenity-js.org'
// - `tagline`, which evaluates to text content of the h1 element
},

// values to inspect (any number of values, to be evaluated one by one):
Page.current().url(),
Text.of(PageElement.located(By.css('h1')).describedAs('tagline'))
),
);

When the actor reaches the interaction to Debug in their workflow, the interaction:

  • instructs the actor to attempt to resolve provided arguments one by one,
  • passes them to your debuggerFunction as an array of DebuggingResults, as well as individual values so that they're easier to inspect and interact with in your debugger,
  • makes the actor collect data artifacts with the results of the evaluation and emit collected artifacts as domain events to all the registered Serenity/JS reporting services so that they're included in your test reports.

Error handling​

When resolving provided argument results in an error, the interaction to Debug will still pass, but the results parameter will contain the details of the error so that you can debug it in your IDE:

import { actorCalled, Debug, Question } from '@serenity-js/core'

const throws = () =>
Question.about('argument that throws an error', actor => {
throw new Error('example error')
})

await actorCalled('Debbie').attemptsTo(
Debug.values(
// debuggerFunction:
(results, value) => {
// set a breakpoint anywhere inside the function to view:
// - `value`, which will be `undefined` since resolving it resulted in an error
// - `results`, which will contain one entry:
// [{
// description: `argument that throws an error`, <- description of the Question
// value: undefined,
// error: Error <- Error thrown during resolution
// }]
},
throws(),
),
);

When resolving more than one argument results in an error, results array provides details of all the errors.

Embedding in Tasks​

Note that the interaction to Debug can be embedded in other tasks, to help with debugging nested workflows:

import { actorCalled, Debug, Task } from '@serenity-js/core'
import { Navigate, PageElement, By, Text } from '@serenity-js/web'

const browseSerenityDocs = () =>
Task.where(`#actor browses Serenity/JS documentation`,
Navigate.to('https://serenity-js.org'),
Debug.values(
// debuggerFunction:
(results, tagline) => {
// set a breakpoint anywhere inside this function
},
Text.of(PageElement.located(By.css('h1')).describedAs('tagline'))
),
)

await actorCalled('Debbie').attemptsTo(
browseSerenityDocs(),
);

Exploring Playwright locators​

Playwright Test for VSCode provides features that allow for experimenting with web UI locators while the test is paused at breakpoint.

Since this functionality is specific to Playwright, you can use it by passing PlaywrightPage.current().nativePage() as one of the arguments. You must also name the evaluated value page, as this is the variable name that the Playwright VSCode extension looks for:

import { actorCalled, Debug } from '@serenity-js/core'
import { PlaywrightPage } from '@serenity-js/playwright'
import { Navigate } from '@serenity-js/web'

await actorCalled('Debbie').attemptsTo(
Navigate.to('https://serenity-js.org'),
Debug.values((results, page) => {
// set a breakpoint here to use Playwright locator debugging features
page.locator('h1')
// note that you can change this selector while having the test paused at breakpoint
}, PlaywrightPage.current().nativePage()),
);

Inspecting page element HTML​

When debugging Page Element Query Language expressions it can be helpful to inspect the HTML of the elements that match the query.

To inspect the HTML of a single PageElement, use its html() instance method:

import { actorCalled, Log } from '@serenity-js/core'
import { Navigate, PageElement, By, Text } from '@serenity-js/web'
import { includes } from '@serenity-js/assertions'

await actorCalled('Debbie').attemptsTo(
Navigate.to('https://serenity-js.org'),

Log.the(
PageElement.located(By.css('h1')).html()
),

Log.the(
PageElements.located(By.css('a'))
.where(Text, includes('modular'))
.first()
.html()
),
);

To inspect the HTML of multiple matching PageElements, use PageElements#eachMappedTo instance method, combined with the meta-question produced by the PageElement.html() static method:

import { actorCalled, Log } from '@serenity-js/core'
import { Navigate, PageElement, By, Text } from '@serenity-js/web'
import { includes } from '@serenity-js/assertions'

await actorCalled('Debbie').attemptsTo(
Navigate.to('https://serenity-js.org'),

Log.the(
PageElements.located(By.css('a'))
.eachMappedTo(PageElement.html())
),

Log.the(
PageElements.located(By.css('a'))
.where(Text, includes('modular'))
.eachMappedTo(PageElement.html())
),
);

Inspecting Serenity/JS domain events​

Serenity/JS uses domain events to facilitate communication between actors and stage crew members (a.k.a. reporting services).

If you're developing a custom StageCrewMember, or suspect a bug in Serenity/JS itself, you can inspect those events using the StreamReporter.