Questions
A "question" in one of the five building blocks of the Screenplay Pattern.
Answering a question enables the actor to retrieve information about the state of the system under test or its execution environment.
Such information can then be passed to an interaction, asserted on, or used to control flow of the scenario.

Implementing Questions
Serenity/JS modules provide you with dozens of questions
you can use in your acceptance tests. You'll find the ones you'll need in your Web-based tests in the @serenity-js/protractor
module, and the ones dedicated to REST API-based tests in @serenity-js/rest
.
A Question.about
...
The Question.about
factory method is the easiest way to define a custom question.
Let's say that we wanted to define a question called NameOfTheActor()
that returned the name of the actor who answers it.
Here's how we could go about it:
import { Actor, Question } from '@serenity-js/core';
const NameOfTheActor = () =>
Question.about('the name of the actor', (actor: Actor) => actor.name);
As you can see above, there are only three things you need to define a custom question:
- the name of the function that will create the question, in this case
NameOfTheActor()
, - a description of the question's subject, here
'the name of the actor'
, which will be used when reporting on the actor answering this question, - a question body, which is a function that receives an
Actor
and returns an answer to the question - here:(actor: Actor) => actor.name
.
While in the above example we return the answer to the question directly, a much more common and idiomatic approach is to use the actor's abilities to produce it.
For example, a question to retrieve the title of the current page could be designed to use the ability to BrowseTheWeb
:
import { Question } from '@serenity-js/core';
import { BrowseTheWeb } from '@serenity-js/protractor';
const WebsiteTitle = () =>
Question.about('the title of the website', actor =>
BrowseTheWeb.as(actor).getTitle() // returns Promise<string>
);
In the above example we retrieve actor's ability to BrowseTheWeb
and then use that to perform the lower-level call.
Please note: Instead of implementing your own custom question to retrieve the title of the website it's better to use Website.title()
Serenity/JS already ships with.
PRO TIP: The function provided as a question body can return an answer either synchronously or asynchronously using a Promise.
Questions and Interactions
Most Serenity/JS interactions can be parameterised, and those that can be parameterised will accept both synchronous and asynchronous questions, as well as regular static and Promise
d values - referred to collectively as Answerable
s.
This design gives you an incredibly flexible and powerful mechanism to form the foundation of your tests. It also means
that you don't need to worry if a given interaction is synchronous or asynchronous, or if a given question returns a static value or a Promise
- Serenity/JS will synchronise them all for you and help you keep your code free of callback mess.
To illustrate how questions and interactions work together, let's look at Target
- a question we use to identify interactive elements on a website.
Consider the below HTML form component, allowing readers to add a comment to an article on an imaginary website:
<form id="new-comment">
<div>
<label for="comment">Comment:</label>
<textarea id="comment"></textarea>
</div>
<div>
<label for="name">Name:</label>
<input type="text" id="name" />
</div>
<div>
<input type="submit" />
</div>
</form>
To interact with the component, we define a Lean Page Object called NewComment
:
import { Target } from '@serenity-js/core';
import { by } from 'protractor';
class NewComment {
static Form =
Target.the('"new comment" form')
.located(by.id('new-comment'));
static NameField =
Target.the('name field')
.located(by.id('name'));
static CommentField =
Target.the('comment field')
.located(by.id('comment'));
static SubmitButton =
Target.the('submit button')
.of(NewComment.Form) // <- a nested Target
.located(by.css('input[type="submit"]'));
}
PRO TIP:
All those Target.the
statements above create questions that, when answered by the actor,
resolve to ElementFinder
objects (or in case of
Target.all
-
ElementArrayFinder
objects),
which are Protractor's extensions of the regular
WebElement
class coming from the selenium-webdriver
module.
Now, to fill out the form, we define a sequence of parameterised interactions and give them to an actor to perform:
import { actorCalled } from '@serenity-js/core';
import { BrowseTheWeb, Enter, Click } from '@serenity-js/protractor';
import { protractor } from 'protractor';
actorCalled('Alice')
.whoCan(BrowseTheWeb.using(protractor.browser))
.attemptsTo(
Enter.theValue('Nice website!') // <- a static value
.into(NewComment.CommentField), // <- a Target passed to an interaction to Enter
Enter.theValue(NameOfTheActor()) // <- our custom question
.into(NewComment.NameField), // returning a static value
Click.on(NewComment.SubmitButton), // <- a Target passed to an interaction to Click
);
There are several interesting things about the code samples above:
- the interaction to
Enter.theValue
accepts both a regular string ('Nice website!'
) and the custom questionNameOfTheActor()
we implemented earlier; most Serenity/JS interactions have this capability, - the
NewComment.SubmitButton
is defined as a "nested target", so aTarget
relative to anotherTarget
. You can see more examples of this design in unit tests.
Mapping the answers
So now you know how to retrieve information about the system under test and its execution environment, but what if this information needs some processing before it can be used further?
Let's say for example that we have the following widget, describing a discount a customer would get on our website:
<div id="order-summary">
<!-- other entries of the order summary -->
<span data-test="percentage-discount">7.5%</span>
</div>
We can, of course, get its text using the Text.of
question and then pass it to an interaction like Ensure
(responsible for performing assertions):
import { Ensure, equals } from '@serenity-js/assertions';
import { actorCalled } from '@serenity-js/core';
import { Target, Text } from '@serenity-js/protractor';
import { by } from 'protractor';
class OrderSummary {
static DiscountWidget =
Target.the('discount widget')
.located(by.css('[data-test="percentage-discount"]'));
}
actorCalled('Alice').attemptsTo(
Ensure.that(Text.of(OrderSummary.DiscountWidget), equals('7.5%')),
);
In many cases, asserting on a text value of an element is perfectly fine, but what if our test needed to check if the discount applied is less than 10%?
We'd have to first convert the text value to a number, wouldn't we?
That's where Question#map
comes into play!
import { Ensure, isLessThan } from '@serenity-js/assertions';
import { actorCalled, trim, replace, toNumber } from '@serenity-js/core';
import { Target, Text } from '@serenity-js/protractor';
import { by } from 'protractor';
class OrderSummary {
static DiscountWidget =
Target.the('discount widget')
.located(by.css('[data-test="percentage-discount"]'));
static DiscountPercentage =
Text.of(OrderSummary.DiscountWidget)
.map(trim()) // <- remove leading and trailing whitespace
.map(replace('%', '')) // <- remove the '%' character
.map(toNumber()) // <- map what's left to a `number`
}
actorCalled('Alice').attemptsTo(
Ensure.that(OrderSummary.DiscountPercentage, isLessThan(10)), // compare as number rather than string
);
There are several interesting things demonstrated in the code sample above:
trim()
,replace()
,toNumber()
and other mapping functions come from the@serenity-js/core
module, which means they're applicable to any question, and not just the Web-specific ones,.map()
calls can be chained.
There's more to .map()
method, though:
- it accepts custom functions, which can be either:
- synchronous:
.map(actor => value => value.replace('%', ''))
, - or asynchronous:
.map(actor => value => Promise.resolve(value.replace('%', ''))
,
- synchronous:
- it works on any question, including the custom ones, i.e.
NameOfTheActor().map(toUpperCase())
- you can use the same technique to map both a question returning a single value, but also a question returning a list of values. This is particularly useful when you need to apply the same transformation to all the rows in an HTML table, or all entries returned in an API response
- all the parameterised mapping functions accept
Answerable
s too.
PRO TIP:
The interaction to Ensure
is responsible for performing assertions and comes from the @serenity-js/assertions
module.
You'll learn more about them in the next chapter.
Changing the subject
One of the great things about Serenity/JS is how it reports the activities performed by the actors.
For example:
OrderSummary.DiscountWidget
gets reported as"the discount widget"
,Text.of(OrderSummary.DiscountWidget)
becomes"text of the discount widget"
, and so on.
However, this also means that in more complex cases this description can become quite a mouthful.
Consider the below example that uses nested targets:
import { trim, replace, toNumber } from '@serenity-js/core';
import { Target, Text } from '@serenity-js/protractor';
import { by } from 'protractor';
class OrderSummary {
static Widget =
Target.the('order summary')
.located(by.id('order-summary'));
static DiscountWidget =
Target.the('discount widget')
.of(OrderSummary.Widget) // <- nested Target
.located(by.css('[data-test="percentage-discount"]'));
static DiscountPercentage =
Text.of(OrderSummary.DiscountWidget)
.map(trim()) // <- mappings
.map(replace('%', ''))
.map(toNumber())
}
In this case, OrderSummary.DiscountPercentage
would get reported as "text of the discount widget of the order summary"
.
To override this automatically-generated description of the question's subject with a custom one, use Question#describedAs()
:
class OrderSummary {
// other fields omitted for brevity
static DiscountPercentage =
Text.of(OrderSummary.DiscountWidget)
.map(trim())
.map(replace('%', ''))
.map(toNumber())
.describedAs('discount percentage') // <- subject name override
}