Questions¶
Questions query the observable state of the system under test. An Actor asks a Question and verifies the answer against an expected value or a predicate.
Why Questions?¶
In the Screenplay pattern, assertions are expressed through the Actor:
This keeps the test narration human-readable ("Ali should see that the Dashboard Title is 'Welcome, Ali'") and ensures every assertion is captured as a domain event in the report.
Class-Based Questions¶
Subclass Question and implement answered_by():
from screenwright import Question
class DashboardTitle(Question[str]):
"""What is the title shown on the dashboard?"""
def answered_by(self, actor) -> str:
client = actor.ability_to(AuthenticateWithAPI).client
token = actor.recalls("auth_token")
return client.get_dashboard(token)["title"]
The generic type parameter (Question[str]) declares the type of the answer. This helps with IDE autocompletion and type checking.
Naming Convention¶
Like Tasks, the Question base class generates a human-readable name from the class name. DashboardTitle becomes "Dashboard Title" in the report narration.
The @question Decorator¶
For simpler Questions, use the @question decorator:
from screenwright import question
@question
def dashboard_title(actor) -> str:
"""What is the title shown on the dashboard?"""
client = actor.ability_to(AuthenticateWithAPI).client
token = actor.recalls("auth_token")
return client.get_dashboard(token)["title"]
Usage:
The function name is converted to title case for the report. dashboard_title becomes "Dashboard Title".
Verifying Answers¶
The should_see_that() method accepts two arguments:
- The Question (any
Answerable) - A matcher: either a literal value or a callable predicate
Equality Check¶
Pass a literal value to compare with ==:
If the answer does not equal the expected value, an AssertionError is raised with a descriptive message:
Predicate Check¶
Pass a callable that takes the answer and returns True or False:
ali.should_see_that(ItemCount(), lambda count: count > 0)
ali.should_see_that(
ResponseStatus(),
lambda status: status in (200, 201),
)
If the predicate returns False, an AssertionError is raised:
Questions with Parameters¶
Questions can accept constructor arguments to parameterize the query:
class TextOf(Question[str]):
def __init__(self, target) -> None:
self.target = target
def answered_by(self, actor) -> str:
driver = actor.ability_to(BrowseTheWeb).driver
return driver.find_element(*self.target.locator).text
Usage:
ali.should_see_that(TextOf(HEADING), "Dashboard")
ali.should_see_that(TextOf(WELCOME_MESSAGE), "Welcome, Ali")
With the decorator:
@question
def text_of(actor, target) -> str:
driver = actor.ability_to(BrowseTheWeb).driver
return driver.find_element(*target.locator).text
Usage:
The Answerable Protocol¶
Both Question and decorator-based questions implement the Answerable protocol:
You can create your own answerable objects without inheriting from Question -- just implement answered_by().
Events Emitted¶
When an Actor asks a Question:
- A
QuestionAskedevent is emitted with the question name - The question's
answered_by()method is called - A
QuestionAnsweredevent is emitted with a summary of the answer and the duration - The matcher is applied; if it fails, an
AssertionErroris raised
Both events appear in the report narration, showing what was asked and what the system answered.