Skip to content

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:

ali.should_see_that(DashboardTitle(), "Welcome, Ali")

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:

ali.should_see_that(dashboard_title(), "Welcome, Ali")

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:

  1. The Question (any Answerable)
  2. A matcher: either a literal value or a callable predicate

Equality Check

Pass a literal value to compare with ==:

ali.should_see_that(DashboardTitle(), "Welcome, Ali")

If the answer does not equal the expected value, an AssertionError is raised with a descriptive message:

Ali expected Dashboard Title to be 'Welcome, Ali', but got: 'Hello, Ali'

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:

Ali expected Item Count to match, but got: 0

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:

ali.should_see_that(text_of(HEADING), "Dashboard")

The Answerable Protocol

Both Question and decorator-based questions implement the Answerable protocol:

class Answerable(Protocol[T]):
    def answered_by(self, actor: Actor) -> T: ...

You can create your own answerable objects without inheriting from Question -- just implement answered_by().

Events Emitted

When an Actor asks a Question:

  1. A QuestionAsked event is emitted with the question name
  2. The question's answered_by() method is called
  3. A QuestionAnswered event is emitted with a summary of the answer and the duration
  4. The matcher is applied; if it fails, an AssertionError is raised

Both events appear in the report narration, showing what was asked and what the system answered.