ScreenPy Recipes

This collection contains examples of common ScreenPy use-cases.

Retrieving Initial Answers

Some tests may need to ensure something has changed. You are able to retrieve the answers to Questions anywhere you may need to.

empty_todo_list_text = Text.of_the(TODO_LIST).answered_by(Wanda)

when(Wanda).attempts_to(AddTodoListItem("Wash the fish"))

then(Wanda).should(
    See.the(Text.of_the(TODO_LIST), DoesNot(ReadExactly(empty_todo_list_text)),
)

Using MakeNote

You can also retrieve initial answers with the MakeNote class, and retrieve the value with a direction.

when(Cameron).attempts_to(
    StartRecording(),
    MakeNote.of_the(Text.of_the(OPENING_LINE))).as_("camera 2 cue"),
    CutToCamera(2).after_line(1),
)

then(Dirk).should(
    See.the(JumpCutLine(), ReadsExactly(the_noted("camera 2 cue"))),
)

Gotchas

There is one limitation to using MakeNote: it is not possible to make a note and retrieve it in the same Actions list.

# CAN NOT do this:
when(Perry).attempts_to(
    ...
    MakeNote.of_the(Text.of_the(NOTABLE_ITEM)).as_("potent notable"),
    Enter.the_text(noted_under("potent notable")).into_the(INPUT_FIELD),
    ...
)

# Workaround:
when(Perry).attempts_to(
    ...
    MakeNote.of_the(Text.of_the(NOTABLE_ITEM)).as_("potent notable"),
)
and_(Perry).attempts_to(
    Enter.the_text(noted_under("potent notable")).into_the(INPUT_FIELD),
    ...
)

This limitation exists because the note isn’t written down until the action is performed. noted_under() will attempt to retrieve the note as the actions list is being passed in to attempts_to(), which is too quick!

See issue #51 for more details (and frustration).

The Eventually Class

If you know an Action or Task will eventually complete, but may need a few tries, you can use the Eventually Action!

This Action takes a performable and performs it, repeatedly, until either it succeeds or the timeout expires. Here’s what that looks like.

Marcel.attempts_to(
    Eventually(
        See.the(Text.of_the(CALL_STATUS), ReadsExactly("Completed!"))
    )
)

The above will repeatedly attempt the assertion that the Call Status section reads “Completed!” until either it does, or 20 seconds (by default) have elapsed. If the timeout expires, a TimeoutError will be raised from the caught error.

Debugging

The Debug Class

You can use the Debug Action to drop a debugger in a series of Actions.

You will need to go up a few frames to get to the Actor’s attempts_to() method. From there, you will be able to step through each Action one at a time.

given(Perry).was_able_to(
    Click.on_the(LOGIN_LINK),
    Enter.the_text(USERNAME).into_the(USERNAME_FIELD),
    Enter.the_password(PASSWORD).into_the(PASSWORD_FIELD),
    Debug(),  # gives you a debugger here!
    Click.on_the(SIGN_IN_BUTTON),
    Wait(60).seconds_for_the(WELCOME_BANNER),
)

The Pause Class

You can also use Pause to stop the test for a few moments, if you only need to see what the state of the page is.

given(Perry).was_able_to(
    Click.on_the(LOGIN_LINK),
    Enter.the_text(USERNAME).into_the(USERNAME_FIELD),
    Enter.the_password(PASSWORD).into_the(PASSWORD_FIELD),
    Pause.for_(60).seconds_because("I need to see something"),  # stops the execution here for 60 seconds.
    Click.on_the(SIGN_IN_BUTTON),
    Wait(60).seconds_for_the(WELCOME_BANNER),
)

Cleaning Up

Sometimes, your Actors may need one or more of their Abilities to do some cleanup. You can assign cleanup tasks to your Actor using their has_ordered_cleanup_tasks() or has_independent_cleanup_tasks() method.

Perry = AnActor.named("Perry").who_can(BrowseTheWeb.using_firefox())
Perry.has_ordered_cleanup_tasks(CompleteAllTodoItems())

# ... test code here

Perry.cleans_up()  # you can call the cleanup method directly
Perry.exit()  # or it is called here automatically

These tasks can be assigned at any point before the Actor exits. Some opportune moments are when the Actor is created, or during a test or task which creates things that need to be cleaned up.

Once the cleanup tasks are performed, they are removed from the Actor’s cleanup list. They will only be performed once.

Using Silently

“Talk less. Smile more.” – Aaron Burr in Hamilton

Sometimes you only need logging when things go wrong. Silently() gives you the capability to only log the important things when things go right. Everything inside of Silently is prevented from logging.

Example: The following Action:

class PerformChatty(Performable):
    @beat("{} tries to PerformChatty")
    def perform_as(self, actor: Actor):
        actor.will(PerformA())

# used inside a test
def test_1(marcel):
    marcel.will(PerformChatty())

Would generate this log:

Marcel tries to PerformChatty
    Marcel tries to PerformA
        Marcel tries to PerformB
            Marcel tries to PerformPass
                Marcel sees if simpleQuestion is equal to True.
                    Marcel examines SimpleQuestion
                        => True
                    ... hoping it's equal to True.
                        => <True>

But what if we didn’t need to know all the steps being taken in PerformChatty unless they were to fail? Wrapping PerformA in Silently

class PerformChatty(Performable):
    @beat("{} tries to PerformChatty")
    def perform_as(self, actor: Actor):
        actor.will(Silently(PerformA()))

…will only generate this log:

Marcel tries to PerformChatty

Unless of course something bad happens inside of PerformA in which case the normal logging will take place:

Marcel tries to PerformChatty
    Marcel tries to PerformA
        Marcel tries to PerformB
            Marcel tries to PerformPass
                Marcel sees if simpleQuestion is equal to True.
                    Marcel examines SimpleQuestion
                        => True
                    ... hoping it's equal to False.
                        => <False>
                    ***ERROR***

AssertionError:
Expected: <True>
     but: was <False>

Using Either

Sometimes you may need to use a try/except control flow in your test, for one reason or another. Luckily, your Actor can perform this flow with the Either Action!

the_actor.will(Either(DoAction()).or_(DoDifferentAction())

The Actor will attempt to perform the first action (or set of actions). If successful, the Actor moves on. But if an AssertionError is raised the Actor will begin performing the second action (or set of actions) passed into or_(). Note the Actor only catches AssertionError here allowing for other exceptions to still be raised.

Other exceptions can be caught when specified.

the_actor.will(
    Either(DoAction())
    .or_(DoDifferentAction())
    .ignoring(ValueError, AssertionError)
)

Either allows users to pass in multiple actions. This is similar Actor performing multiple actions in one call.

the_actor.will(
    Either(
        DoAction1(),
        DoAction2(),
        DoAction3(),
    ).or_(
        DoDifferentAction1(),
        DoDifferentAction2(),
        DoDifferentAction3(),
    )
)

Note

Either will not describe any cleanup for the Actor after it experiences a failure in the first routine; the Actor will proceed directly to the second routine. Keep this in mind while defining the two branches of Actions.

To help illustrate this further here is a real-world example using screenpy_selenium

the_actor.will(
   Either(
       See(BrowserURL(), ReadsExactly(URL)),
       CheckIfAuthenticated(),
   ).or_(
       ClearCache()
       Open.their_browser_on(URL())
       Eventually(Enter(username).into(USERNAME_FIELD)),
       Enter.the_secret(password).into(PASSWORD_FIELD),
       Click.on(SIGN_IN_BUTTON)
   )
)