Welcome to Stere’s documentation!¶
Documentation¶
Getting Started¶
Requirements¶
Python >= 3.6
Installation¶
Stere is currently in a proof-of-concept stage and is not available on pypi. It can be installed with pip using the following command:
pip install git+git://github.com/jsfehler/stere.git#egg=stere
Setup¶
Stere.browser¶
Stere requires a browser (aka driver) to work with. This can be any class that ultimately drives automation. Pages, Fields, and Areas inherit their functionality from this object.
Here’s an example with Splinter:
from stere import Stere
from splinter import Browser
Stere.browser = Browser()
As long as the base Stere object has the browser set, the browser’s functionality is passed down to everything else.
Pages¶
The Page class is the base which all Page Objects should inherit from.
Although inheriting from Page is not required for Fields or Areas to work, Page will act as a proxy for calls to the browser attribute.
Using Splinter’s browser.url method as an example, the following methods are analogous:
MyPage.url == MyPage.browser.url == browser.url
The choice of which syntax to use depends on how you want to write your tests.
Fields¶
The Field objects represent individual elements on a web page. Conceptually, they represent general behaviours, not specific HTML elements.
The following Fields are available with the default Splinter implementation:
- Button: Clickable object.
- Checkbox: Object with a set and unset state.
- Dropdown: Object with multiple options to choose from.
- Input: Object that accepts keyboard input.
- Link: Clickable text.
- Root: Parent container.
- Text: Non-interactive text.
Fields take 2 arguments: location strategy and locator.
self.some_text = Text('xpath', '//*[@id="js-link-box-pt"]/small/span')
Performer method¶
A Field can have a single method be designated as a performer. This causes the method to be called when the Field is inside an Area and that Area’s perform() method is called.
For example, Input’s performer is the fill() method, and Button’s performer is the click() method. Given the following:
self.search = Area(
query=Input('id', 'xsearch'),
submit=Button('id', 'xsubmit'),
)
When search.perform() is called, query.fill() is called, followed by submit.click().
See the documentation for Area for more details.
Assigning the performer method¶
When creating a new type of Field, the stere_performer class decorator can be used to assign a performer method.
from stere.fields.field import stere_performer
@stere_performer('philosophize', consumes_arg=False)
class DiogenesButton(Field):
def philosophize(self):
print("As a matter of self-preservation, a man needs good friends or ardent enemies, for the former instruct him and the latter take him to task.")
The consumes arg argument should be used to specify if the method should use an argument provided by Area.perform() or not.
Field.includes(value)¶
Will search every element found by the Field for a value property that matches the given value. If an element with a matching value is found, it’s then returned.
Useful for when you have non-unique elements and know a value is in one of the elements, but don’t know which one.
PetStore().inventory_list.includes("Kittens").click()
Field.before()¶
This method is called automatically before methods with the @use_before decorator are called. By default it does nothing. It can be overridden to support any desired behaviour.
In this example, Dropdown has been subclassed to hover over the Dropdown before clicking.
from stere.fields import Dropdown
class CSSDropdown(Dropdown):
"""A Dropdown that's customized to hover over the element before attempting
a select.
"""
def before(self):
self.element.mouse_over()
Field.after()¶
This method is called automatically after methods with the @use_after decorator are called. By default it does nothing. It can be overridden to support any desired behaviour.
Subclassing Field¶
Field can be subclassed to suit your own requirements.
If the __init__() method is overwritten, make sure to call super() before your own code.
If your class needs specific behaviour when interacting with Areas, it must implement the perform() method.
Button¶
A simple wrapper over Field, it implements click() as its performer.
click()¶
Clicks the element.
Checkbox¶
By default, the Checkbox field works against HTML inputs with type=”checkbox”.
Can be initialized with the default_checked argument. If True, the Field assumes the checkbox’s default state is checked.
It implements opposite() as its performer.
set_to(state)¶
Set a checkbox to the desired state.
- Args:
- state (bool): True for check, False for uncheck
toggle()¶
If the checkbox is checked, uncheck it. If the checkbox is unchecked, check it.
opposite()¶
Switches the checkbox to the opposite of its default state. Uses the default_checked attribute to decide this.
Dropdown¶
By default, the Dropdown field works against HTML Dropdowns. However, it’s possible to extend Dropdown to work with whatever implementation of a CSS Dropdown you need.
It implements select() as its performer.
The option argument can be provided to override the default implementation. This argument expects a Field. The Field should be the individual options in the dropdown you wish to target.
self.languages = Dropdown('id', 'langDrop', option=Button('xpath', '/h4/a/strong'))
options¶
Searches for all the options in the dropdown and returns a list of Fields.
select(value)¶
Searches for an option with value, then clicks it.
Input¶
A simple wrapper over Field, it implements fill() as its performer.
fill(value)¶
Fills the element with value.
Link¶
A simple wrapper over Field, it implements click() as its performer.
click()¶
Clicks the element.
Root¶
A simple wrapper over Field, it does not implement a performer method.
Text¶
A simple wrapper over Field, it does not implement a performer method.
Location Strategies¶
These represent the way a locator will be searched for.
By default, the strategies available are:
- css
- xpath
- tag
- name
- text
- id
- value
These all use Splinter. If you’re using a different automation tool, you must create your strategies. These can override the default strategies. (ie: You can create a custom css strategy to replace the default)
Custom Locator Strategies¶
Custom strategies can be defined using the @strategy decorator on top of a Class.
Any class can be decorated with @strategy, as long as the _find_all and _find_all_in_parent methods are implemented.
In the following example, the ‘data-test-id’ strategy is defined. It wraps Splinter’s find_by_xpath method to simplify the locator required on the Page Object.
from stere.strategy import strategy
@strategy('data-test-id')
class FindByDataTestId():
def is_present(self, *args, **kwargs):
return self.browser.is_element_present_by_xpath(f'.//*[@data-test-id="{self.locator}"]')
def is_not_present(self, *args, **kwargs):
return self.browser.is_element_not_present_by_xpath(f'.//*[@data-test-id="{self.locator}"]')
def _find_all(self):
"""Find from page root."""
return self.browser.find_by_xpath(f'.//*[@data-test-id="{self.locator}"]')
def _find_all_in_parent(self):
"""Find from inside parent element."""
return self.parent_locator.find_by_xpath(f'.//*[@data-test-id="{self.locator}"]')
With this implemented, Fields can now be defined like so:
my_button = Button('data-test-id', 'MyButton')
Areas¶
Areas represent groupings of Fields on a Page.
The following Area objects are available:
- Area: A non-hierarchical, unique group of Fields.
- RepeatingArea: A hierarchical, non-unique group of Areas. They require a Root Field.
Area()¶
The Area object takes any number of Fields as arguments. Each Field must be unique on the Page and only present in one Area.
from stere.areas import Area
from stere.fields import Input
class MyPage():
def __init__(self):
self.my_area = Area(
my_input=Input('xpath', '//my_xpath_string')
)
Fields in an Area can be called as attributes of the Area:
def test_stuff():
MyPage().my_area.my_input.fill('Hello world')
Area.perform()¶
The perform method will “Do the right thing” sequentially for every Field inside an Area.
- For Button and Link, it will click them.
- For Input, it will fill them using the text arguments provided.
- For Text, it will do nothing.
from stere.areas import Area
from stere.fields import Input
class MyPage():
def __init__(self):
self.my_area = Area(
my_input=Input('xpath', '//my_xpath_string'),
my_input_2=Input('xpath', '//my_xpath_string'),
my_button=Button('xpath', '//my_xpath_string')
)
def test_stuff():
MyPage().my_area.perform('Hello', 'World')
RepeatingArea()¶
A RepeatingArea represents a collection of Fields that appear multiple times on a Page.
The RepeatingArea objects requires a Root Field in the arguments, but otherwise takes any number of Fields as arguments. The other Fields will use the Root as a parent.
from stere.areas import RepeatingArea
from stere.fields import Root, Input
class MyPage():
def __init__(self):
self.my_repeating_area = RepeatingArea(
root=Root('xpath', '//my_xpath_string'),
my_input=Input('xpath', '//my_xpath_string')
)
RepeatingArea().area_with()¶
Takes two arguments: A Field’s name and an expected value. Searches the RepeatingArea for a single Area where the Field’s value matches the expected value and then returns the entire Area object.
class Inventory():
def __init__(self):
self.items = RepeatingArea(
root=Root('xpath', '//my_xpath_string'),
description=Text('xpath', '//my_xpath_string')
)
def test_stuff():
found_area = MyPage().items.area_with("description", "Bananas")
RepeatingArea().areas¶
A list of all the Area objects found can be accessed with the areas attribute.
def test_stuff():
listings = MyPage().my_repeating_area.areas
listings[0].my_input.fill('Hello world')
Reusing Areas¶
Sometimes an identical Area may be present on multiple pages. Areas do not need to be created inside a page object, they can be created outside and then called from inside a page.
header = Area(
...
)
class Items(Page):
def __init__(self, *args, **kwargs):
self.header = header
Subclassing Areas¶
If an Area appears on many pages and requires many custom methods, it may be better to subclass the Area instead of embedding the methods in the Page Object:
class Header(Area):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def my_custom_method(self, *args, **kwargs):
...
class Main(Page):
def __init__(self, *args, **kwargs):
self.header = Header()
class Other(Page):
def __init__(self, *args, **kwargs):
self.header = Header()
Workflows¶
When working with an Area that has multiple possible routes, there may be Fields which you do not want the .perform() method to call under certain circumstances.
Take the following example Page Object:
class AddSomething(Page):
def __init__(self):
self.form = Area(
item_name=Input('id', 'itemName'),
item_quantity=Input('id', 'itemQty'),
save=Button('id', 'saveButton'),
cancel=Button('id', 'cancelButton')
)
Calling AddSomething().form.perform() would cause the save button and then the cancel button to be acted on.
In these sorts of cases, Workflows can be used to manage which Fields are called.
class AddSomething(Page):
def __init__(self):
self.form = Area(
item_name=Input('id', 'itemName', workflows=["success", "failure"]),
item_quantity=Input('id', 'itemQty', workflows=["success", "failure"]),
save=Button('id', 'saveButton', workflows=["success"]),
cancel=Button('id', 'cancelButton', workflows=["failure"])
)
Calling AddSomething().form.workflow(“success”).perform() will ensure that only Fields with a matching workflow are called.
Best Practices¶
A highly opinionated guide. Ignore at your own peril.
Favour composition over inheritance¶
When building Page Objects for something with many reused pieces (such as a settings menu) don’t build an abstract base Page Object. Build each component separately and call them in Page Objects that reflect the application.
Inheritance:
class BaseSettings(Page):
def __init__(self):
self.menu = Area(...)
class SpecificSettings(BaseSettings):
def __init__(self):
super().__init__()
Composition:
from .another_module import settings_menu
class SpecificSettings(Page):
def __init__(self):
self.menu = settings_menu
Explanation:
Doing so maintains the benefits of reusing code, but prevents the creation of Page Objects that don’t reflect actual pages in an application.
Creating abstract Page Objects to inherit from can make it confusing as to what Fields are available on a page.
Single blank line when changing page object¶
Wrong:
def test_the_widgets():
Knicknacks.menu.gadgets.click()
Knicknacks.gadgets.click()
Gadgets.add_widgets.click()
Gadgets.add_sprocket.click()
Right:
def test_the_widgets():
Knicknacks.menu.gadgets.click()
Knicknacks.gadgets.click()
Gadgets.add_widgets.click()
Gadgets.add_sprocket.click()
Explanation:
Changing pages usually indicates a navigation action. Using a consistent line break style visually helps to indicate the steps of a test.