Welcome to ScreenPy’s documentation!¶
ScreenPy provides a solid, SOLID base for writing maintainable test suites following the Screenplay Pattern, popularized by Antony Marcano. It also provides nice test logging through Allure and support for BDD-style natural language test case writing.
Installation¶
To install ScreenPy, run the following command, preferably in a virtual environment:
pip3 install screenpy
This will also install the screenpy-quickstart
script
and the following dependencies:
- Selenium
- PyHamcrest
- Allure’s Pytest plugin
- Pytest
Quickstart¶
screenpy-quickstart¶
To quickly set up
a Screenplay Pattern test suite using ScreenPy,
cd
to the folder you will use for your suite
and run this command:
screenpy-quickstart
This will set up
user_interface
,
questions
,
tasks
, and
features
directories
and fill them with a simple test.
For an explanation of each of these directories,
see the File Hierarchy page!
File Hierarchy¶
The key to a good Screenplay Pattern suite is understanding how the files all fit together. The hierarchy described herein is one example of how the files can be organized and named. If your team feels strongly that there are better conventions to follow, renaming the files will not break any of ScreenPy’s functionality.
Here is an example hierarchy:
- suite_root
- features # this is where the actual test files will live
- feature1.py
- …
- questions # questions your actors will ask about the site
- question1.py
- …
- tasks # groups of actions your actors can perform, with descriptive names
- task1.py
- …
- user_interface # files containing locators and/or URLs for each page
- page1.py
- …
- requirements.txt # where you list screenpy!
- features # this is where the actual test files will live
Features¶
The feature films! The story arcs! The whole point of the suite! These are the features of your application that you are testing; this is where all the actual test files go.
Questions¶
Things your actor asks about the application, to perform a thrilling turnabout (test fail) or a cathartic confirmation (test pass) upon finding the answer. These files are where you will access elements on the page to figure out if your test has passed.
For more information, see the Questions page!
Included in ScreenPy¶
ScreenPy comes with a lot of the base tools you will need to get started, which should cover the most common use cases. You’ll be set up in time to make curtain call!
Actors¶
Actors are the do-ers in the screenplay. Actors set the scene, perform their hearts out, and then make dramatic assertions that will either see a happy ending or a tragic failure.
More seriously, the actors represent the users of your application, doing the things you’d expect them to do on it (or things you might not expect them to do). Screenplay Pattern focuses entirely on what your users hope to do on your site, so your test cases will focus on what the actors do, which includes gaining Abilities, performing Actions, and asking Questions.
Using Actors¶
To instantiate a new actor, just give it a name:
from screenpy.actor import Actor, AnActor
Perry = AnActor.named("Perry")
Without any abilities, your actor will be woefully unprepared to begin their performance. To give your actor an ability, you can do something like:
from selenium.webdriver import Firefox
from screenpy.abilities import BrowseTheWeb
Perry.can(BrowseTheWeb.using(Firefox()))
# For convenience, you can also do the same like this
Perry = AnActor.named("Perry").who_can(BrowseTheWeb.using(Firefox()))
Now, Perry is able to attempt any actions that require the ability to BrowseTheWeb. Attempting actions looks like this:
from screenpy import Target
from screenpy.actions import Click
EXAMPLE_LINK = Target.the("example link").located_by("//a")
Perry.attempts_to(Click.the(EXAMPLE_LINK))
You’ll notice we had to make a quick Target
there.
We’ll get to Targets later,
but a quick summary
is that they’re how you tell the actors
where to perform the action.
In the above example,
the action knows
what ability it requires,
and it will ask the actor
to find its matching ability
to perform the action.
If the actor
does not have that ability,
the actor will raise an UnableToPerformError
.
Now that our actor has performed an action, they are ready to perform a test. Tests are performed with Questions, like so:
from screenpy.questions import Text
from screenpy.resolutions import ReadsExactly
THE_WELCOME_MESSAGE = Target.the("welcome_message").located_by("span.welcome")
Perry.should_see_the((Text.of(THE_WELCOME_MESSAGE), ReadsExactly("Welcome!"))
That’s the whole flow! Your actor is now ready to exit:
Perry.exits_stage_right()
Actor Class¶
-
class
screenpy.actor.
Actor
(name: str)¶ Represents an actor, holding their name and abilities. Actors are the performers of your screenplay, they represent your users as they go about their business on your product.
An actor is meant to be instantiated using its static
named()
method. A typical invocation might look like:Perry = Actor.named(“Perry”)This will create the actor, ready to take on their first role.
-
ability_to
(ability: Any) → Any¶ Syntactic sugar for
uses_ability_to()
.
-
attempts_to
(*actions) → None¶ Performs a list of actions, one after the other.
Parameters: actions – the list of actions to perform.
-
exit
() → None¶ The actor forgets all of their abilities, ready to assume a new role when their next cue calls them.
-
static
named
(name: str) → screenpy.actor.Actor¶ Names this actor, logs their entrance, and returns the instance.
Parameters: name – the name of this new Actor. Returns: Actor
-
should_see
(*tests) → None¶ Syntactic sugar for
should_see_the()
.
-
should_see_that
(*tests) → None¶ Syntactic sugar for
should_see_the()
.
-
should_see_the
(*tests) → None¶ Asks a series of questions, asserting that the expected answer resolves.
Parameters: tests – tuples of a Questions and a Resolutions. Raises: AssertionError
– If the question’s actual answer does not match the expected answer from the Resolutions.
-
uses_ability_to
(ability: Any) → Any¶ Finds the ability referenced and returns it, if the actor is able to do it.
Parameters: ability – the ability to retrieve. Returns: The requested ability. Raises: |UnableToPerformError|
– the actor doesn’t possess the ability.
-
was_able_to
(*actions) → None¶ Syntactic sugar for
attempts_to()
.
-
Abilities¶
Abilities allow your Actor
to do things.
Actors will leverage their abilities
to perform actions
that require those abilities.
Using Abilities¶
To give an actor an ability,
pass it in using the actor’s who_can()
or can()
methods:
from screenpy import Actor, AnActor
from screenpy.abilities import BrowseTheWeb
# Add abilities on instantiation
Perry = AnActor.named("Perry").who_can(BrowseTheWeb.using_firefox())
# Or add abilities later
Perry = AnActor.named("Perry")
Perry.can(BrowseTheWeb.using_safari())
Granting an ability to an actor
allows them to perform any Actions
or ask any Questions
that require that ability.
If an action or a question require an ability
that the actor does not have,
the actor will raise an UnableToPerformError
.
Writing New Abilities¶
There may be other abilities your actors need to possess
in order to test your application.
You are encouraged to write your own!
The only prescribed method for an ability
is the forget
method,
which will complete any cleanup required.
For an example,
see the forget()
method
of the BrowseTheWeb ability.
A base class for Abilities is provided for convenience:
screenpy.abilities.base_ability.BaseAbility
Included Abilities¶
BrowseTheWeb¶
-
class
screenpy.abilities.browse_the_web.
BrowseTheWeb
(browser: selenium.webdriver.remote.webdriver.WebDriver)¶ The ability to browse the web with a web browser. This ability is meant to be instantiated with its
using()
static method, which takes in the WebDriver to use, or one of its other “using” methods. A typical invocation looks like:BrowseTheWeb.using(selenium.webdriver.Firefox())
BrowseTheWeb.using_firefox()
This will create the ability that can be passed in to an actor’s
who_can()
method.-
find
(locator: Union[Target, Tuple[selenium.webdriver.common.by.By, str]]) → selenium.webdriver.remote.webelement.WebElement¶ Syntactic sugar for
to_find()
.
-
find_all
(target: Union[Target, Tuple[selenium.webdriver.common.by.By, str]]) → selenium.webdriver.remote.webelement.WebElement¶ Syntactic sugar for
to_find_all()
.
-
forget
() → None¶ Asks the actor to forget how to BrowseTheWeb. This quits the connected browser.
An actor who is exiting will forget all their abilities.
-
to_find
(target: Union[Target, Tuple[selenium.webdriver.common.by.By, str]]) → selenium.webdriver.remote.webelement.WebElement¶ Locates a single element on the page using the given locator.
Parameters: target – the Target
or tuple describing the element.Returns: WebElement Raises: |BrowsingError|
-
to_find_all
(target: Union[Target, Tuple[selenium.webdriver.common.by.By, str]]) → List[selenium.webdriver.remote.webelement.WebElement]¶ Locates many elements on the page using the given locator.
Parameters: target – the Target
or tuple describing the elements.Returns: List[WebElement]
-
to_get
(url: str) → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Uses the connected browser to visit the specified URL.
This action supports using the BASE_URL environment variable to set a base URL. If you set BASE_URL, the url passed in to this function will be appended to the end of it. For example, if you have BASE_URL=http://localhost, then to_get(“/home”) will send your browser to “http://localhost/home”.
If BASE_URL isn’t set, then the passed-in url is assumed to be a fully qualified URL.
Parameters: url – the URL to visit. Returns: BrowseTheWeb
-
to_switch_to
(target: Target) → None¶ Switches the browser context to the target.
Parameters: target – the Target
or tuple describing the element to switch to.
-
to_switch_to_alert
() → selenium.webdriver.common.alert.Alert¶ Switches to an alert and returns it.
Returns: Alert Raises: |BrowsingError|
– no alert was present to switch to.
-
to_switch_to_default
() → None¶ Switches the browser context back to the default frame.
-
to_wait_for
(target: Union[Target, Tuple[selenium.webdriver.common.by.By, str]], timeout: int = 20, cond: Callable = <class 'selenium.webdriver.support.expected_conditions.visibility_of_element_located'>)¶ Waits for the element to fulfill the given condition.
Parameters: - target – the tuple or
Target
describing the element. - timeout – how many seconds to wait before raising a TimeoutException. Default is 20.
- cond – the condition to wait for. Default is visibility_of_element_located.
Raises: |BrowsingError|
– the target did not satisfy the condition in time.- target – the tuple or
-
static
using
(browser: selenium.webdriver.remote.webdriver.WebDriver) → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Specifies the driver to use to browse the web. This can be any WebDriver instance, even a remote one.
Parameters: browser – the webdriver instance to use. Returns: BrowseTheWeb
-
static
using_android
() → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Creates an uses a default Remote driver instance to connect to a running Appium server and open Chrome on Android. Use this if you don’t need to set anything up for your test browser.
Note that Appium requires non-trivial setup to be able to connect to Android emulators. See the Appium documentation to get started: http://appium.io/docs/en/writing-running-appium/running-tests/
- Environment Variables:
- APPIUM_HUB_URL: the URL to look for the Appium server. Default
- is “http://localhost:4723/wd/hub”
- ANDROID_DEVICE_VERSION: the version of the device to put in
- the desired capabilities. Default is “10.0”
- ANDROID_DEVICE_NAME: the device name to request in the desired
- capabilities. Default is “Android Emulator”
Returns: BrowseTheWeb
-
static
using_chrome
() → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Creates and uses a default Chrome Selenium webdriver instance. Use this if you don’t need to set anything up for your test browser.
Returns: BrowseTheWeb
-
static
using_firefox
() → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Creates and uses a default Firefox Selenium webdriver instance. Use this if you don’t need to set anything up for your test browser.
Returns: BrowseTheWeb
-
static
using_ios
() → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Creates an uses a default Remote driver instance to connect to a running Appium server and open Safari on iOS. Use this if you don’t need to set anything up for your test browser.
Note that Appium requires non-trivial setup to be able to connect to iPhone simulators. See the Appium documentation to get started: http://appium.io/docs/en/writing-running-appium/running-tests/
- Environment Variables:
- APPIUM_HUB_URL: the URL to look for the Appium server. Default
- is “http://localhost:4723/wd/hub”
- IOS_DEVICE_VERSION: the version of the device to put in the
- desired capabilities. Default is “13.1”
- IOS_DEVICE_NAME: the device name to request in the desired
- capabilities. Default is “iPhone Simulator”
Returns: BrowseTheWeb
-
static
using_safari
() → screenpy.abilities.browse_the_web.BrowseTheWeb¶ Creates and uses a default Safari Selenium webdriver instance. Use this if you don’t need to set anything up for your test browser.
Returns: BrowseTheWeb
-
wait_for
(locator: Union[Target, Tuple[selenium.webdriver.common.by.By, str]], timeout: int = 20, cond: Callable = <class 'selenium.webdriver.support.expected_conditions.visibility_of_element_located'>)¶ Syntactic sugar for
to_wait_for()
.
-
AuthenticateWith2FA¶
-
class
screenpy.abilities.authenticate_with_2fa.
AuthenticateWith2FA
(otp: pyotp.totp.TOTP)¶ The ability to retrieve a one-time password from a two-factor authenticator. This ability is meant to be instantiated with its
using_secret()
method, which will take in the 2FA secret, or itsusing()
static method, which takes in an instantiated PyOTP instance. A typical invocation looks like:AuthenticateWith2FA.using_secret(“KEEPITSECRETKEEPITSAFE”)
AuthenticateWith2FA.using(pyotp_instance)
This will create the ability that can be passed in to an actor’s
who_can()
method.-
forget
() → None¶ Cleans up the pyotp instance stored in this ability.
-
to_get_token
() → str¶ Gets the current two-factor token to use as a one-time password.
Returns: str
-
static
using
(otp: pyotp.totp.TOTP) → screenpy.abilities.authenticate_with_2fa.AuthenticateWith2FA¶ Uses an already-created TOTP instance to provide tokens.
Parameters: otp (pyotp.TOTP) – an instance of a TOTP object. Returns: AuthenticateWith2FA
-
static
using_secret
(secret: str) → screenpy.abilities.authenticate_with_2fa.AuthenticateWith2FA¶ Creates a TOTP instance with the given secret.
Parameters: secret – the secret given by the 2FA service. You may need to decode a QR code to get this secret. Returns: AuthenticateWith2FA
-
Targets¶
Targets are a way to encapsulate a human-readable string along with a CSS selector or xpath locator.
To instantiate a target, you might do something like this:
from screenpy import Target
EXAMPLE_ELEMENT1 = Target.the("first example element").located_by("//div")
EXAMPLE_ELEMENT2 = Target.the("second example element").located_by("span.example")
Let’s break that down a little bit.
The class method the()
expects a human-readable string
to give the element
a log-friendly name.
That same class method
returns the newly instantiated Target
object,
ready to have its
located_by()
method called.
The located_by()
method
takes in the actual locator,
which can either be
XPath
or CSS Selector.
Targets are expected to be defined in your user_interface files, and can then be used in your Actions, your Questions, and your Tasks.
Target Class¶
-
class
screenpy.target.
Target
(desc: str)¶ A class to contain information about an element. This class stores a nice human-readable string describing an element along with either an XPath or a CSS selector string. It is intended to be instantiated by calling its static
the()
method. A typical invocation might look like:Target.the(“header search bar”).located_by(“div.searchbar”)It can then be used in Questions, Actions or Tasks to access that element.
-
all_found_by
(the_actor: screenpy.actor.Actor) → List[selenium.webdriver.remote.webelement.WebElement]¶ Gets a list of WebElement objects described by the stored locator.
Parameters: the_actor (Actor) – The Actor
who should look for these elements.Returns: list(WebElement)
-
found_by
(the_actor: screenpy.actor.Actor) → selenium.webdriver.remote.webelement.WebElement¶ Gets the WebElement object representing the targeted element.
Parameters: the_actor (Actor) – The Actor
who should look for this element.Returns: WebElement
-
get_locator
() → Tuple[selenium.webdriver.common.by.By, str]¶ Returns the stored locator as a (By, str) tuple.
Returns: Tuple(By, str) Raises: |TargetingError|
– if no locator was supplied to the target.
-
located
(locator: Tuple[selenium.webdriver.common.by.By, str]) → screenpy.target.Target¶ Supplies an instantiated target with a locator. This locator is a tuple of the By strategy to use and the identifying string, e.g.
Target.the(“signout link”).located((By.LINK_TEXT, “Sign Out”))Parameters: locator – the (By, str) tuple to use to find the element. Returns: Target
-
located_by
(locator: str) → screenpy.target.Target¶ Supplies an instantiated Target with a locator string, which is either a CSS selector or an XPATH string. The strategy will be determined before it is stored.
Parameters: locator – the string to use as a locator for the element. Can be a CSS selector or an xpath string. Returns: Target
-
static
the
(desc: str) → screenpy.target.Target¶ Creates a Target with a description. This method call should be followed up with a call to
located_by()
.Parameters: desc (str) – The human-readable description for the targeted element. Beginning with a lower-case letter makes the allure test logs look the nicest. Returns: Target
-
Actions¶
Actions are the things that an Actor
can do,
using their Abilities.
Using Actions¶
Actions can be used pretty much anywhere.
They will typically be used to create Tasks
or move around in your Features.
Here is an example of using the Click
action:
from screenpy.actions import Click
from ..user_interface.homepage import LOGIN_LINK
Perry.attempts_to(Click.on_the(LOGIN_LINK))
Actors will always only attempt
to perform an action.
They may not actually have the correct Abilities,
after all.
If an actor is unable to perform an action or task,
they will raise an UnableToPerformError
.
Writing New Actions¶
Occasionally,
you might find that the base actions
don’t quite cover
a unique use case you have
for your test suite.
Since Screenplay Pattern is built to be extensible,
it is easy and encouraged
to create your own custom actions
to achieve what you need!
The only requirement for creating more actions
is that they have a perform_as
method defined
which takes in the actor who will perform the action.
A base class for Actions is provided
to ensure the required methods
are defined:
screenpy.actions.base_action.BaseAction
Let’s take a look
at what an extremely contrived custom action,
ChecksTheSpelling
,
might look like:
# actions/checks_the_spelling.py
from screenpy.actions import BaseAction
class ChecksTheSpelling(BaseAction):
@staticmethod
def of_words_in_the(locator):
return ChecksSpelling(locator)
def perform_as(self, the_actor):
the_actor.uses_ability_to(CheckSpelling).to_check()
def __init__(self, locator):
self.locator = locator
ScreenPy attempts to follow a convention
of putting all the static methods first,
then the perform_as
function,
and leaving the dunder methods at the bottom.
This way the most important methods are first
for someone perusing your code.
Tasks¶
Sometimes, your actors might repeat the same series of actions several times. A grouping of common actions can be abstracted into a Task in your Tasks.
A common task for Screenplay Pattern suites is logging in to your application under test. This login task might look something like this:
# tasks/login.py
import os
from screenpy import Actor
from screenpy.actions import BaseAction, Click, Enter
from ..user_interface.homepage import (
SIGN_ON_LINK,
THE_USERNAME_FIELD,
THE_PASSWORD_FIELD,
LOGIN_BUTTON,
)
class LoginSuccessfully(BaseAction):
"""
Log in to the application successfully.
"""
@staticmethod
def using_credentials(username: str, password: str) -> "LoginSuccessfully":
"""
Supply the credentials for the account.
Args:
username: the username to use.
password: the password to use.
"""
return LoginSuccessfully(username, password)
def perform_as(self, the_actor: Actor) -> None:
"""
Asks the actor to log in to the application.
Args:
the_actor: the actor who will perform this task.
Raises:
UnableToPerformError: the actor does not have the ability to
BrowseTheWeb.
"""
the_actor.attempts_to(
Click.on(SIGN_ON_LINK),
Wait.for_the(THE_USERNAME_FIELD).to_appear(),
Enter.the_text(self.username).into(THE_USERNAME_FIELD),
Enter.the_text(self.password).into(THE_PASSWORD_FIELD),
Click.on_the(LOGIN_BUTTON)
)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
And there you have it!
Now all you have to do
is ask your actor
to attempt to LoginSuccessfully
,
and you’ve got the same set of actions everywhere.
Note that tasks,
just like actions,
are required to have a perform_as
method defined.
You can use the BaseAction
class
for tasks as well.
Provided Actions¶
Open¶
-
class
screenpy.actions.open.
Open
(location: Union[str, object])¶ A very important action; opens the browser! An Open action is expected to be instantiated via its static
browser_on()
method. A typical invocation might look like:Open.browser_on(the_homepage_url)
Open.browser_on(HomepageObject)
If you pass in an object, make sure the object has a url property that can be referenced by this action.
It can then be passed along to the
Actor
to perform the action.-
static
browser_on
(location: Union[str, object]) → screenpy.actions.open.Open¶ Creates a new Open action which holds its destined location.
Parameters: location – The URL to open when this action is performed, or an object containing a url property that holds the URL to open when this action is performed. Returns: Open
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the supplied actor to perform this Open action, using their ability to browse the web.
Parameters: the_actor – The Actor
who will perform the action.Raises: |UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
static
their_browser_on
(location: Union[str, object]) → screenpy.actions.open.Open¶ Syntactic sugar for
browser_on()
.
-
static
Click¶
-
class
screenpy.actions.click.
Click
(target: screenpy.target.Target)¶ Clicks on an element! A Click action is expected to be instantiated via its static
on()
oron_the()
methods. A typical invocation might look like:Click.on_the(PROFILE_LINK)It can then be passed along to the
Actor
to perform the action.-
static
on
(target: screenpy.target.Target) → screenpy.actions.click.Click¶ Syntactic sugar for
on_the()
.
-
static
on_the
(target: screenpy.target.Target) → screenpy.actions.click.Click¶ Creates a new Click action with its crosshairs aimed at the provided target.
Parameters: target – The Target
describing the element to click.Returns: Click
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to find the element described by the stored target, and then clicks it. May wait for another target to appear, if
then_wait_for()
had been called.Parameters: the_actor – the
Actor
who will perform the action.Raises: |DeliveryError|
– an exception was raised by Selenium.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
then_wait_for
(target: screenpy.target.Target) → screenpy.actions.click.Click¶ Syntactic sugar for
then_wait_for_the()
.
-
then_wait_for_the
(target: screenpy.target.Target) → screenpy.actions.click.Click¶ Supplies a target to wait for after performing the click.
This method has been deprecated as of version 1.0.0. Please use the included
Wait
action instead. This method will be removed in version 2.0.0.Parameters: target – The Target
describing the element to wait for after performing the click.Returns: Click
-
static
Clear¶
-
class
screenpy.actions.clear.
Clear
(target: screenpy.target.Target)¶ Clears the text from an input field. A Clear action is expected to be instantiated by its static
the_text_from()
method. A typical invocation might look like:Clear.the_text_from(COMMENT_FIELD)It can then be passed along to the
Actor
to perform the action.-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to performs the Clear action, clearing the text from the targeted input field using their ability to browse the web.
Parameters: the_actor – The Actor
who will perform this action.Raises: |UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
static
the_text_from
(target: screenpy.target.Target) → screenpy.actions.clear.Clear¶ Syntactic sugar for
the_text_from_the()
.
-
Enter¶
-
class
screenpy.actions.enter.
Enter
(text: str, mask: bool = False)¶ Enters text into an input field. An Enter action is expected to be instantiated by its static
the_text()
method. A typical invocation might look like:Enter.the_text(“Hello world!”).into(COMMENT_FIELD)It can then be passed along to the
Actor
to perform the action.-
into
(target: screenpy.target.Target) → screenpy.actions.enter.Enter¶ Supplies the target to enter the text into. This is most likely an input field.
Parameters: target – The Target
describing the input field.Returns: Enter
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the Enter action, entering the text into the targeted input field using their ability to browse the web.
If this Enter object’s
then_hit()
method was called, it will also hit the supplied keys. Finally, if thethen_wait_for()
method was called, it will wait for the supplied target to appear.Parameters: the_actor – the
Actor
who will perform this action.Raises: |DeliveryError|
– an exception was raised by Selenium.|UnableToActError|
– no target was supplied.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
static
the_keys
(text: str) → screenpy.actions.enter.Enter¶ Syntactic sugar for
the_text()
.
-
static
the_password
(text: str) → screenpy.actions.enter.Enter¶ Syntactic sugar for
the_secret()
.
-
static
the_secret
(text: str) → screenpy.actions.enter.Enter¶ Creates a new Enter action with the provided text, but will mask the text for logging. The text will appear as “[CENSORED]” in the report. It is expected that the next call will be to the instantiated Enter object’s
into()
method.Parameters: text – the text to enter into the target, but it’s a secret. Returns: Enter
-
static
the_text
(text: str) → screenpy.actions.enter.Enter¶ Creates a new Enter action with the provided text. It is expected that the next call will be to the instantiated Enter object’s
into()
method.Parameters: text – the text to enter into the target. Returns: Enter
-
then_hit
(*keys) → screenpy.actions.enter.Enter¶ Supplies additional keys to hit after entering the text, for example if the keyboard ENTER key should be pressed.
Parameters: keys – the keys to hit afterwards. These are probably the constants from Selenium’s Keys, but they can be strings if you know the codes. Returns: Enter
-
then_press
(*keys) → screenpy.actions.enter.Enter¶ Syntactic sugar for
then_hit()
.
-
then_wait_for
(target: screenpy.target.Target) → screenpy.actions.enter.Enter¶ Supplies the target to wait for after entering text (and hitting any additional keys, if this object’s
then_hit()
method was called).This method has been deprecated as of version 1.0.0. Please use the included
Wait
action instead. This method will be removed in version 2.0.0.Parameters: target – the Target
to wait for after entering text.Returns: Enter
-
Enter2FAToken¶
-
class
screenpy.actions.enter_2fa_token.
Enter2FAToken
(target: screenpy.target.Target)¶ Enters the current two-factor authentication token into an input field. An Enter2FAToken action is expected to be instantiated by its static
into_the()
method. A typical invocation might look like:Enter2FAToken.into_the(2FA_INPUT_FIELD)It can then be passed along to the
Actor
to perform the action.-
static
into
(target: screenpy.target.Target) → screenpy.actions.enter_2fa_token.Enter2FAToken¶ Syntactic sugar for
into_the()
-
static
into_the
(target: screenpy.target.Target) → screenpy.actions.enter_2fa_token.Enter2FAToken¶ Provide the input field into which to enter the 2FA token.
Parameters: target – the Target
describing the input field.Returns: Enter2FAToken
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the Enter2FAToken action, which will get the current token using the actor’s AuthenticateWith2FA ability.
Parameters: the_actor – the Actor
who will perform this action.Raises: |UnableToPerformError|
– if the actor does not have the abilities toAuthenticateWith2FA
andBrowseTheWeb
.
-
static
Select¶
-
class
screenpy.actions.select.
Select
¶ Selects an option from a dropdown menu. This is an entry point that will create the correct specific Select action that will need to be used, depending on how the option needs to be selected. Some examples of invocations:
Select.the_option_named(“January”).from_the(MONTH_DROPDOWN)
Select.the_option_at_index(0).from_the(MONTH_DROPDOWN)
Select.the_option_with_value(“jan”).from_the(MONTH_DROPDOWN)
It can then be passed along to the
Actor
to perform the action.-
static
the_option_at_index
(index: Union[int, str]) → screenpy.actions.select.SelectByIndex¶ Instantiate a SelectByIndex class which will select the option at the specified index. This index is 0-based.
Parameters: index – the index (0-based) of the option to select. Returns: SelectByIndex
-
static
the_option_named
(text: str) → screenpy.actions.select.SelectByText¶ Instantiate a SelectByText class which will select the option with the given text.
Parameters: text – the text of the option to select. Returns: SelectByText
-
static
the_option_with_value
(value: str) → screenpy.actions.select.SelectByValue¶ Instantiate a SelectByText class which will select the option with the given text.
Parameters: value – the value of the option to select. Returns: SelectByText
-
static
-
class
screenpy.actions.select.
SelectByText
(text: str, target: Optional[screenpy.target.Target] = None)¶ A specialized Select action that chooses the option by text. This class is meant to be accessed via the Select action’s static
the_option_named()
method. A typical invocation might look like:Select.the_option_named(“January”).from_the(MONTH_DROPDOWN)It can then be passed along to the
Actor
to perform the action.-
from_
(target: screenpy.target.Target) → screenpy.actions.select.SelectByText¶ Syntactic sugar for
from_the()
.
-
from_the
(target: screenpy.target.Target) → screenpy.actions.select.SelectByText¶ Provides the target to select the option from.
Parameters: target – the Target
describing the dropdown or multi-select element to select the option from.Returns: SelectByText
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to attempt to find the dropdown element described by the stored target, then performs the select action.
Parameters: the_actor – The
Actor
who will perform the action.Raises: |DeliveryError|
– an exception was raised by Selenium.|UnableToActError|
– no target was supplied.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
-
class
screenpy.actions.select.
SelectByIndex
(index: Union[int, str], target: Optional[screenpy.target.Target] = None)¶ A specialized
Select
action that chooses the option by its index. This class is meant to be accessed via the Select action’s staticthe_option_at_index()
method. A typical invocation might look like:Select.the_option_at_index(0).from_the(MONTH_DROPDOWN)It can then be passed along to the
Actor
to perform the action.-
from_
(target: screenpy.target.Target) → screenpy.actions.select.SelectByIndex¶ Syntactic sugar for
from_the()
.
-
from_the
(target: screenpy.target.Target) → screenpy.actions.select.SelectByIndex¶ Provides the target to select the option from.
Parameters: target – The Target
describing the dropdown or multi-select element to select the option from.Returns: SelectByIndex
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to attempt to find the dropdown element described by the stored target, then performs the select action.
Parameters: the_actor – The
Actor
who will perform the action.Raises: |DeliveryError|
– an exception was raised by Selenium.|UnableToActError|
– no target was supplied.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
-
class
screenpy.actions.select.
SelectByValue
(value: Union[int, str], target: Optional[screenpy.target.Target] = None)¶ A specialized Select action that chooses the option by its value. This class is meant to be accessed via the Select action’s static
the_option_with_value()
method. A typical invocation might look like:Select.the_option_with_value(“jan”).from_the(MONTH_DROPDOWN)It can then be passed along to the
Actor
to perform the action.-
from_
(target: screenpy.target.Target) → screenpy.actions.select.SelectByValue¶ Syntactic sugar for
from_the()
.
-
from_the
(target: screenpy.target.Target) → screenpy.actions.select.SelectByValue¶ Provides the target to select the option from.
Parameters: target – The Target
describing the dropdown or multi-select element to select the option from.Returns: SelectByValue
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to attempt to find the dropdown element described by the stored target, then performs the select action.
Parameters: the_actor – The
Actor
who will perform the action.Raises: |DeliveryError|
– an exception was raised by Selenium.|UnableToActError|
– no target was supplied.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
AcceptAlert¶
-
class
screenpy.actions.accept_alert.
AcceptAlert
¶ Accepts an alert. An AcceptAlert action is expected to be instantiated as it is, no static methods for this one. The only invocation looks like:
AcceptAlert()It can then be passed along to the
Actor
to perform the action.-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the AcceptAlert action.
Parameters: the_actor – The
Actor
who will perform this action.Raises: |BrowsingError|
– no alert was present.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
DismissAlert¶
-
class
screenpy.actions.dismiss_alert.
DismissAlert
¶ Dismisses an alert. An DismissAlert action is expected to be instantiated as it is, no static methods for this one. The only invocation looks like:
DismissAlert()It can then be passed along to the
Actor
to perform the action.-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the DismissAlert action.
Parameters: the_actor – The
Actor
who will perform this action.Raises: |BrowsingError|
– no alert was present.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
RespondToThePrompt¶
-
class
screenpy.actions.respond_to_the_prompt.
RespondToThePrompt
(text: str)¶ Responds to a javascript prompt by entering the specified text and accepting the prompt. RespondToThePrompt is expected to be instantiated using its
with_()
static method. A typical instantiation might look like:RespondToThePrompt.with_(“I am big. It’s the pictures that got small.”)It can then be passed along to the
Actor
to perform the action.-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the RespondToPrompt action.
Parameters: the_actor – The
Actor
who will perform this action.Raises: |BrowsingError|
– no alert was present.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
static
with_
(text: str) → screenpy.actions.respond_to_the_prompt.RespondToThePrompt¶ Specifies the text to enter into the prompt.
Parameters: text – the text to enter. Returns: RespondToTheText
-
SwitchTo¶
-
class
screenpy.actions.switch_to.
SwitchTo
(target: Optional[screenpy.target.Target])¶ Switches to something, most likely an iframe, or back to default. A SwitchTo action is expected to be instantiated by its static
the()
ordefault()
methods, or on its own. A typical invocation might look like:SwitchTo.the(ORDERS_FRAME)
SwitchTo.default()
It can then be passed along to the
Actor
to perform the action.-
static
default
() → screenpy.actions.switch_to.SwitchTo¶ Switches back to the default frame, the browser window.
Returns: SwitchTo
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the SwitchTo action.
Parameters: the_actor – The Actor
who will perform this action.Raises: |UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
static
Wait¶
-
class
screenpy.actions.wait.
Wait
(seconds: int = 20, target: Optional[screenpy.target.Target] = None)¶ Waits for an element to fulfill a certain condition. A Wait action is expected to be instantiated by its
for_()
method, followed by one of its strategies. By default, theto_appear()
strategy is used. Wait can also be instantiated with an integer, like Wait(30), which will set the timeout to be used. Some examples of invocations:Wait.for_the(LOGIN_FORM)
Wait.for_the(WELCOME_BANNER).to_contain_text(“Welcome!”)
Wait.for(CONFETTI).to_disappear()
Wait(10).seconds_for_the(PARADE_FLOATS).to_appear()
It can then be passed along to the
Actor
to perform the action.-
static
for_
(target: screenpy.target.Target) → screenpy.actions.wait.Wait¶ Creates a new Wait action holding the provided target.
Parameters: target – The Target
to wait for.Returns: Wait
-
static
for_the
(target: screenpy.target.Target) → screenpy.actions.wait.Wait¶ Syntactic sugar for
for_()
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to perform the Wait action, using the contained strategy and any extra arguments provided.
Parameters: the_actor – The
Actor
who will perform this action.Raises: |UnableToActError|
– no target was supplied.|UnableToPerformError|
– the actor does not have the ability toBrowseTheWeb
.
-
seconds_for
(target: screenpy.target.Target) → screenpy.actions.wait.Wait¶ Sets the target after invoking
Wait
with the number of seconds you want wait to allow the target to fulfill the expected condition. For example:Wait(60).seconds_for(CONFETTI).to_disappear()This will ask the actor to wait up to 60 seconds for CONFETTI to disappear before throwing an exception.
Parameters: target – The Target
to wait for.Returns: Wait
-
seconds_for_the
(target: screenpy.target.Target) → screenpy.actions.wait.Wait¶ Syntactic sugar for
seconds_for()
-
to_appear
() → screenpy.actions.wait.Wait¶ Uses Selenium’s “visibility of element located” strategy. This is the default strategy, so calling this is not strictly necessary.
Returns: Wait
-
to_be_clickable
() → screenpy.actions.wait.Wait¶ Uses Selenium’s “to be clickable” strategy.
Returns: Wait
-
to_contain_text
(text: str) → screenpy.actions.wait.Wait¶ Uses Selenium’s “text to be present in element” strategy.
Parameters: text – the text to expect to be present. Returns: Wait
-
static
Pause¶
-
class
screenpy.actions.pause.
Pause
(number: float)¶ Pauses the actor’s actions for a set amount of time. This class should only be used when absolutely necessary. You must call one of the “…_because” methods to pass in the reason for your pause; an
UnableToActError
will be raised if no reason was given when the actor attempts to perform this action.A Pause action is expected to be instantiated by its static
for_()
method, followed by one of the methods that supply a reason (seconds_because, second_because, or milliseconds_because). A typical invocation might look like:Pause.for_(500).milliseconds_because(“the welcome banner needs to hide.”)It can then be passed along to the
Actor
to perform the action.-
static
for_
(number: int) → screenpy.actions.pause.Pause¶ How many seconds or milliseconds to wait for.
Parameters: number – the number of seconds or milliseconds to sleep for. Returns: Pause
-
milliseconds_because
(reason: str) → screenpy.actions.pause.Pause¶ Tells the Pause instance to use milliseconds and provides a reason for the pause. Hard waits are the worst of all wait strategies, so providing a reason will help explain why it was necessary to use this strategy.
Parameters: reason – the reason for needing to pause. Returns: Pause
-
perform_as
(the_actor: screenpy.actor.Actor) → None¶ Asks the actor to take their union-mandated break.
Parameters: the_actor – the Actor
who will perform this action.Raises: |UnableToActError|
– no reason was supplied.
-
second_because
(reason: str) → screenpy.actions.pause.Pause¶ Syntactic sugar for Pause.seconds_because
-
seconds_because
(reason: str) → screenpy.actions.pause.Pause¶ Tells the Pause instance to use seconds and provides a reason for the pause. Hard waits are the worst of all wait strategies, so providing a reason will help explain why it was necessary to use this strategy.
Parameters: reason – the reason for needing to pause. Returns: Pause
-
static
Debug¶
-
class
screenpy.actions.debug.
Debug
¶ In long chains of actions, it can be difficult to drop a debugger in the right place. This action can be placed anywhere in the chain to give you a debugger in the middle of the action chain. This action uses Python 3.7+’s breakpoint() call if it can, otherwise it will default to pdb.set_trace().
A Debug action is expected to be instantiated in the standard way. A typical instantiation will always look like:
Debug()It can then be passed along to the
Actor
to perform the action.
Questions¶
Questions are asked by an actor about the current state of the page or application. They are the first half (the “actual value”) of ScreenPy’s test assertions (the other half, Resolutions, is next).
Asking Questions¶
Typically, you will not be asking a question without an expected answer. This is how you do test assertions in ScreenPy:
from screenpy.questions import Text
from screenpy.resolutions import ReadsExactly
from ..user_interface.homepage import WELCOME_MESSAGE
Perry.should_see_the((Text.of_the(WELCOME_MESSAGE), ReadsExactly("Welcome!")), )
We’ll talk about Resolutions next,
but that call to should_see_the()
is taking in our question.
Behind the curtain,
our actor is investigating
the current state of the application
(using their ability to BrowseTheWeb
)
to find out what the text actually says
at the locator described by WELCOME_MESSAGE
.
They take that answer
and compare it to the expected answer
passed in by the resolution.
If they match,
a comedy!
Our test passes.
If they do not match,
a tragedy!
Our test fails.
Writing New Questions¶
It is very likely
that you may want to write additional questions,
and you are encouraged to do so!
The only prescribed method
for a question class
is an asked_by
method
that takes in an actor.
This method will do the work
of getting the answer to the question.
For example,
you may want to take a look at
the asked_by()
method
of the Text
class.
A base class for Questions is provided
to ensure the required methods
are defined:
screenpy.questions.base_question.BaseQuestion
Provided Questions¶
List¶
-
class
screenpy.questions.list.
List
(target: screenpy.target.Target)¶ Asks for a list of elements, viewed by an
Actor
. This question is meant to be instantiated using its staticof()
orof_all()
methods. Typical invocations might look like:List.of(SEARCH_RESULTS)
List.of_all(IMAGES)
It can then be passed along to the
Actor
to ask the question.
Number¶
-
class
screenpy.questions.number.
Number
(target: screenpy.target.Target)¶ Asks how many of a certain element are on the page, viewed by an
Actor
. This question is meant to be instantiated via its staticof()
method. A typical invocation might look like:Number.of(SEARCH_RESULTS)It can then be passed along to the
Actor
to ask the question.
Text¶
-
class
screenpy.questions.text.
Text
(target: screenpy.target.Target, multi: bool = False)¶ Asks what text appears in an element or elements, viewed by an
Actor
. This question is meant to be instantiated using its staticof()
orof_all()
methods. Typical invocations might look like:Text.of(THE_WELCOME_HEADER)
Text.of_all(SEARCH_RESULTS)
It can then be passed along to the
Actor
to ask the question.
Selected¶
-
class
screenpy.questions.selected.
Selected
(target: screenpy.target.Target, multi: bool = False)¶ Answers questions about what options are selected in dropdowns, multi-select fields, etc, viewed by an
Actor
. This question is meant to be instantiated using its staticoption_from
oroption_from
methods. Typical invocations might look like:Selected.option_from(THE_STATE_DROPDOWN)
Selected.options_from(INDUSTRIES)
It can then be passed along to the
Actor
to ask the question.
Resolutions¶
Resolutions provide an expected answer to questions. They are the second half of test assertions in ScreenPy: the “expected value”. (The first half are Questions, if you missed that page.)
Using Resolutions¶
Like Questions,
you probably will not use
a resolution directly.
You will typically pass a resolution
along with a question
into your actor’s
should_see_the()
method:
from screenpy.questions import Text
from screenpy.resolutions import ReadsExactly
from ..user_interface.homepage import WELCOME_MESSAGE
Perry.should_see_the((Text.of_the(WELCOME_MESSAGE), ReadsExactly("Welcome!")))
In that line of code,
ReadsExactly
is returning
a PyHamcrest matcher.
It will be evaluated later
as should_see_the()
does its job.
If the expected value
(“Welcome!”)
matches the actual value
retrieved by our question,
bravo!
Our test passes.
If they do not match,
boo!
Our test fails.
Writing New Resolutions¶
Resolutions are really
just an abstraction barrier
for the truly excellent
PyHamcrest library.
To add your own resolutions,
create your resolution class
by inheriting from the
BaseResolution
class.
All you need to provide in your resolution
is a line
class property,
which is just a human readable string
for the log,
and then to define
the __init__
method.
The custom Resolution’s
__init__
method
will need to set
the expected value,
and instantiate the PyHamcrest matcher
that your resolution is masking.
For several examples,
see the documentation
of the Provided Resolutions below.
Wait Strategies¶
Automated test scripts are fast. When a test runs quickly, sometimes it can try to act on an element that isn’t quite ready or hasn’t even been drawn yet. ScreenPy allows you to use each of the prominent waiting strategies.
You can also reference Selenium’s “Waits” documentation for more information.
Explicit Waits¶
ScreenPy provides a Wait
function to wait for certain elements to appear, to be clickable, to contain text, or to disappear. These are included as a convenience because they are the most common strategies required. If those strategies aren’t enough, you can also pass in your own strategy. Here are some examples of how this action can be used:
from screenpy.actions import Wait
# waits 20 seconds for the sign in modal to appear
Perry.attempts_to(Wait.for_the(LOGIN_MODAL))
# waits 42 seconds for the welcome banner to disappear
Perry.attempts_to(Wait(42).seconds_for(THE_WELCOME_BANNER).to_disappear())
# waits 20 seconds for a custom expected condition
Perry.attempts_to(Wait.for_the(PROFILE_ICON).using(appears_in_greyscale))
Implicit Waits¶
Implicit waiting is handled at the driver level. This is the less preferred method of waiting, but it can be useful in some situations, and does prevent a lot of Wait actions being littered around your action chains. Before you pass the driver in, you can set the implicit wait timeout like so:
from selenium.webdriver import Firefox
driver = Firefox()
driver.implicitly_wait(30)
Perry = AnActor.who_can(BrowseTheWeb.using(driver))
Hard Waits¶
This method of waiting is discouraged. However, sometimes you just need to pause the script for a few moments, whether it’s for Debugging or if it’s because of an animation that defies any reasonable method of explicitly waiting for it to complete.
In these situations, as a last resort, ScreenPy offers the Pause
action. Here are some ways to use it:
from screenpy.actions import Pause
Perry.attempts_to(Pause.for_(30).seconds_because("I need to get a new locator."))
Perry.attempts_to(Pause.for_(500).milliseconds_because("the banner animation must finish."))
Debugging¶
Debugging in ScreenPy can sometimes be difficult.
If you’re used to stepping through code using a debugger,
getting to the part where your Actor
is performing their Actions
can be difficult.
To aid in debugging,
the Debug
action class can be used
to drop into a debugger
in the middle of any action chain!
It hooks into Python 3.7+’s breakpoint function if it can,
so you can modify your preferred debugger
and turn debugging off
by manipulating the PYTHONBREAKPOINT environment variable.
You can read more about this excellent new function
by perusing PEP553.
As for the action class, here’s an example of an action chain:
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),
Click.on_the(SIGN_IN_BUTTON),
Wait(60).seconds_for_the(WELCOME_BANNER),
)
If we know we have some issue after entering the username and password, but before clicking the sign in button, we can add a Debug() call there:
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),
)
Now the test will drop us into either
your chosen debugger
or pdb.
You’ll need to return a couple times
to get back up to the Actor
class’s attempts_to()
method.
From there,
you can step through the rest of the actions
one at a time,
or dive into one if you need to!
Alternative Method¶
If you just need the actor to hold on a second
while you verify the state of the webpage,
you can use the Pause
action instead,
like so:
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_(20).seconds_because("I need to see something"), # stops the execution here for 20 seconds.
Click.on_the(SIGN_IN_BUTTON),
Wait(60).seconds_for_the(WELCOME_BANNER),
)
Exceptions¶
There are several exceptions thrown about in ScreenPy. Mostly they are used to provide extra context when other exceptions are raised.
Ability Exceptions¶
-
class
screenpy.exceptions.
AbilityError
¶ These errors are raised when an ability fails in some way.
-
class
screenpy.abilities.browse_the_web.
BrowsingError
¶ Raised when BrowseTheWeb encounters an error.
Action Exceptions¶
-
class
screenpy.exceptions.
ActionError
¶ These errors are raised when an action fails.
-
class
screenpy.exceptions.
DeliveryError
¶ Raised when an action encounters an error while being performed.
-
class
screenpy.exceptions.
UnableToActError
¶ Raised when an action is missing direction.
Additional Context¶
Screenplay Pattern uses composition instead of inheritance to form the test suite. The concept was first formed by Antony Marcano— frustrated with Page Object Model files growing unreasonably large— under the name the Journey Pattern.
You can watch Antony’s talk about Screenplay Pattern at SeleniumConf2016, which is the same talk that got me interested in this pattern!
You can also see some documentation about screenplay pattern from the folks who made SerenityBDD, the library that ScreenPy is modeled after: The Screenplay Pattern - Serenity/JS Handbook