Skip to content

Tasks and Interactions

Tasks vs. Interactions

Task Interaction
Level High-level, business-meaningful Low-level, atomic
Composition Can contain other Tasks and Interactions Leaf node; directly operates on the system
Example "Log in" "Click on the login button"
Base class Task Interaction

Both Tasks and Interactions implement the Performable protocol, which requires a single method:

def perform_as(self, actor: Actor) -> None: ...

An Actor performs them with attempts_to():

actor.attempts_to(Login.with_credentials("admin", "secret123"))

Class-Based Tasks

Subclass Task and implement perform_as():

from screenwright import Task


class Login(Task):
    def __init__(self, username: str, password: str) -> None:
        self.username = username
        self.password = password

    @staticmethod
    def with_credentials(username: str, password: str) -> "Login":
        return Login(username, password)

    def perform_as(self, actor) -> None:
        actor.attempts_to(
            Navigate.to("/login"),
            Enter.the_value(self.username).into(USERNAME_FIELD),
            Enter.the_value(self.password).into(PASSWORD_FIELD),
            Click.on(LOGIN_BUTTON),
        )

Notice how the Task composes lower-level Interactions (Navigate, Enter, Click). This is the key advantage of the Screenplay pattern: Tasks express intent at the business level, while Interactions handle the mechanics.

Naming Convention

The Task base class automatically generates a human-readable name from the class name by inserting spaces before uppercase letters. Login becomes "Login", AddItemToCart becomes "Add Item To Cart". This name appears in the test report narration.

The @task Decorator

For simpler Tasks, you can use the @task decorator instead of a class:

from screenwright import task


@task
def login(actor, username: str, password: str) -> None:
    actor.attempts_to(
        Navigate.to("/login"),
        Enter.the_value(username).into(USERNAME_FIELD),
        Enter.the_value(password).into(PASSWORD_FIELD),
        Click.on(LOGIN_BUTTON),
    )

Usage:

actor.attempts_to(login(username="admin", password="secret123"))

The decorator converts the function name to a title-case label for the report. login becomes "Login", add_item_to_cart becomes "Add Item To Cart".

Class-Based Interactions

Subclass Interaction and implement perform_as():

from screenwright import Interaction


class Click(Interaction):
    def __init__(self, target) -> None:
        self.target = target

    @staticmethod
    def on(target) -> "Click":
        return Click(target)

    def perform_as(self, actor) -> None:
        driver = actor.ability_to(BrowseTheWeb).driver
        element = driver.find_element(*self.target.locator)
        element.click()

Interactions differ from Tasks in that they:

  • Directly access an Ability (like BrowseTheWeb)
  • Perform a single atomic operation on the system under test
  • Are not composed of other performables

The target Attribute

If an Interaction has a target attribute, Screenwright includes the target description in the InteractionStarted event. This appears in the report narration, for example: "Click on LOGIN_BUTTON".

Composing Tasks

Tasks gain their power from composition. A high-level Task can orchestrate a sequence of lower-level Tasks and Interactions:

class PlaceOrder(Task):
    def __init__(self, product_name: str, quantity: int) -> None:
        self.product_name = product_name
        self.quantity = quantity

    def perform_as(self, actor) -> None:
        actor.attempts_to(
            SearchFor(self.product_name),
            AddToCart(quantity=self.quantity),
            Checkout(),
            ConfirmPayment(),
        )

Each sub-Task and Interaction emits its own events, creating a nested narration in the report. The event hierarchy is maintained through parent_id fields on the events, so the report can show that SearchFor and AddToCart happened as part of PlaceOrder.

Performing Multiple Actions

attempts_to() accepts any number of performables:

actor.attempts_to(
    Navigate.to("/products"),
    SearchFor("laptop"),
    AddToCart(quantity=1),
    Checkout(),
)

They are executed in order. If any performable raises an exception, execution stops and the error is captured as a TaskFailed or InteractionFailed event.

Error Handling

When a Task or Interaction fails:

  1. The duration is recorded
  2. A TaskFailed or InteractionFailed event is emitted with the error type and message
  3. The original exception is re-raised

You do not need to add try/except blocks in your Tasks or Interactions. Screenwright handles error capture automatically.