Tests (Functional) Package

Submodules

Test Browsers

Functional Testing with Selenium

The tests in seed.functional.tests.test_browser use Selenium to test things actually work in a browser. There are a number of things you need to know before adding tests.

..warning:

Ignore this at your peril!

In order to test against multiple browsers without having to duplicate Test Classes the Test Cases, the classes they subclass from, and all the tests themselves are all generated on the fly.

The tests are written in such a way that, hopefully, adding tests, will be familiar, and little different from normal. If all you want to do is add a test, providing you bear in mind the caveats below, you should not need to concern your self with how things actually work behind the scenes. Follow the instructions and you can write one test method and it will automatically run against multiple browsers.

Adding a test

test_browser.py contains two basic test classes:

LoggedOutTests
this contains test methods that run when a user is logged out
LoggedInTests
this contains test methods that run when a user is logged in

These should be sufficient for most cases and you can just add your test as normal.

Warning

These are not defined in the top level scope

Rather they are defined within the equivalent generator. e.g. loggedout_tests_generator. This is the key to the behind the scenes magic, the generator invokes the class multiple times, each time sub-classing it from a different base class. These are identical but for the fact that they use a different browser to power the tests.

As long as you respect this, and indent your methods properly, you can write your tests as normal. You should also read the section, Running Tests, however, as the tests are not run in the normal way, so the error message generated by a failing test may surprise you.

Adding a browser specific test.

... WARNING:: Test detection, internally, is as smart as say Nose, tests must begin with `test_` to be recognized. Define your test as normal but put anything browser specific behind a guard condition. To do this you can test against the browser name:

if self.browser_type.name == 'Firefox':

The Browser Definition that defines this name is contained in browser_definition.py.

You can also add tests that will only be invoked by Travis using:

if os.getenv('TRAVIS') == 'true':

When tests are run locally(i.e. not on Travis) only cross platform browsers will be used. If you need to write a browser specific test against anything else, and you wish to run the test locally, you can override this by setting the environment ‘SEED_TEST_BROWSER’ to one of IE, SAFARI, EDGE. This will cause that browser to be used as well. You will need to ensure you have the relevant driver installed.

Running Tests.

Tests can be run as normal, but a failing test may produce different output from what you are expecting. In particular the FAIL: statement will list the name of the generator, not the test method. So you will see

However the place the test failed can still be derived from the trace back e.g.:

Traceback (most recent call last):
    [snip]...
    File "/home/paulmunday/projects/seed/seed/functional/tests/test_browser.py",    line 341, in test_pass

In addition the tests will print the name of a failed test and the browser used to standard error, though this prints in line with the tests, not the same place as the report from the test runner.

Adding Test Cases to test_browsers.py

The included classes should be sufficient but it is easy enough to add more. Just make sure to wrap you class definition in a generator. You can base this off an existing generator like so:

def my_test_generator():
    for browser in BROWSERS:
        class MyTestClass(LOGGED_IN_CLASSES[browser.name]):
            def my_test(self):
                pass
        Test = LoggedOutTests()
          for test in get_tsts(Test):
              yield test

Lets break that down a little:

` for browser in BROWSERS:` In combination with the next line this will create a copy of your test for each browser. BROWSERS is imported from base.py and is a list of Browser Definitions

`class MyTestClass(LOGGED_IN_CLASSES[browser.name]):` Rather than sub-classing directly this supplies a different base case each time we go around the loop. In this case the base classes are taken from LOGGED_IN_CLASSES. This is imported from base.py: its a list of Base Classes generated there that each use a different browser to power the same base Test Class. (LOGGED_OUT_CLASSES is also defined and it also possible to define other base classes there). This gives us the ability to test against multiple browsers easily. You must inherit from a class defined there.

` Test = LoggedOutTests()` Here we instantiate a copy of out derived test class. Since we invoke it directly, not via the test runner, the setUp methods won’t be called, however get_tsts takes care of this.

`for test in get_tsts(Test): get_tsts takes care of running the setUpClass method for us, then it examines the test class we passed looking for methods it thinks are tests (i.e. their name begins with `test_`). When it finds one it wraps it in a function that the test runner can call (making sure to invoke the setUp and tearDown methods) and will be safe for the generator to yield. (you can’t return a Test Class from the generator, as unittest expects a function).

... Caution :: see setUp and tearDown below if you intend to override these.

setUp and tearDown

If you want to override these you need to be aware there are not invoked by the test runner. Instead the function yielded by the generator invokes them manually . You must take care to reverse anything you did in the setup (especially anything that calls a Model) or stuff will break in unpredictable ways. You should be doing this regardless anyway. Also be sure to call super as this takes care of opening and closing the browser.

How to Write Tests

Within the Selenium world there is a design pattern known as Page Objects. A Page object “represents an area in the web application user interface that your test is interacting”, with the idea that this produces reusable code that only needs to be fixed in one place.

Essentially each page has its own associated Page Object class. These are used to load the page and query it. The SEED Selenium testing framework makes extensive use of these. Initiating a page object verifies the page object and has the ability to create any database records needed for the page to function.

Each page object has the same find_element(s)_by... Functionality as Web Elements (and the Browser/Webdriver) so you any page element you find using these also has the same methods. It also provides easy access to ActionChains methods for more complex browser interactions. Since these are all just wrappers around the Selenium Webdriver and Action Chain methods you can consult the existing Python-Selenium documentation for more information. http://selenium-python.readthedocs.io.

Page objects also have wait_for_element methods. These correspond to the equivalent find_element methods but contain Explicit Waits to allow time for the element to appear. These can be used just after a page is loaded, or for AJAX loaded elements to make sure they are present before querying them. These work in the same way as the corresponding find_element method. Note they can only locate a single element, the first found, in the same way as the find_element methods. There is no equivalent of the find_elements methods. To emulate these use a wait_for_element method to return the parent container then query this using find_elements.

In addition, since SEED has lots of tabular data, it extends the Page Object design pattern with the notion a Table object. Querying tables can be tricky with Selenium. Typically you need to identify a particular cell by XPath and this can be a laborious process. To compound this table structure is not always fixed in SEED so what columns and what order they will be displayed in will vary per user/organization.

Table objects aim to get around these limitations by providing an easily query-able representation of a table. This will be explained in more detail below, but essentially a Table Object stores each row in a Table Row object. A Table Row is a wrapper round an Ordered Dict that uses the table header as keys so you can access a particular cell by its table header without worrying about its position. In addition it can also be accessed by index (i.e. column number). So in this example:

Food Cost $ | Quantity
Bananas 1 20
Apples 0.5 10
Pears 0.8 20

table[1][‘Food’] will return Bananas, as will table[1][0]

There are additional methods to locate a particular row or column for example table.get_row_by_field(‘Food’, ‘Bananas’) returns {‘Food’: ‘Bananas’, ‘Cost’: ‘1’, ‘Quantity: ‘20’}.

Example Tests

Basic Example:

def test_buildings_list(self):
    buildings_list = BuildingsList(self, url=True)

..Note:

the self in BuildingsList(self, url=True) refers to the TestClass object.
We (have to) pass it in in order that the Page object can access its
functionality -- the web driver/browser specifically.
You **must** pass in self like this to use a Page object.

The above is a valid test to ensure that the Buildings list page loads. Instantiating a BuildingsList object with url=True causes the page to be populated with data and checks that it has been loaded. This happens in the __init__ method of BuildingsList so you get it for free when you use BuildingsList. All page objects have something similar).

It is a limited test however: though we have populated the page with data the test doesn’t check the data is valid.

A better version of the test is this:

def test_buildings_list(self):
    buildings_list = BuildingsList(self, url=True)
    table = buildings_list.ensure_table_is_loaded()
    address = table.first_row['ADDRESS LINE 1']
    assert address.text == 'address'

Now the test checks some data is present as well (in this case ‘address’ is the default text set where a building is created if ‘address_line_1’ is not set).

Traversing Pages:

Often we need to test that a page, not only loads but can be reached by a user. Here is a complete example:

def test_building_list_tab_settings(self):
    """Make sure building list settings tab loads."""
    # load buildings list and create records
    buildings_list = BuildingsList(self, url=True)
    # locate setting link and click on it
    settings_link = buildings_list.find_element_by_id('list-settings')
    settings_link.click()

    # ensure settings page has loaded correctly.
    settings_page = BuildingListSettings(self)
    table = settings_page.ensure_table_is_loaded()
    assert table.first_row['COLUMN NAME'].text == 'Address Line 1'

In the first part of the test the Buildings list page is loaded and the BuildingsList page objects find_element_by_id method is used to locate and click on the appropriate record.

The second part creates an instance of BuildingListSettings page object, to check the page is loaded, then uses it to locate and check the table data.

..Note:

**Only** *self* is supplied as a parameter to the page object. When a page object is used in this way it won't create any data and won't load the page directly. It will however check the page has loaded.

Thus we can use it in this way to check that following a link loads the correct page and that the data needed is already in place.

..Note:

If you navigate away form a page and then back you should call the reload()
method of the corresponding page object to ensure it has been loaded,
before interacting with it. Failing to do so will cause problems as
the browser might still be on the previous page.

This won't reload the page itself, you have to do that by following a link.
Because many pages in SEED are not actually separate pages, but rather
a new view of the same page constructed by an AJAX call there is no
way for the page object to do this (at least reliably).

Table Objects

..Note:

Tables and TableRow, TableColumn objects are all immutable by design.

Any time a page contains a (data) table the relevant page object should have a ensure_table_is_loaded method defined. (This is fairly trivial to do and will be covered later). This returns a Table object and should be used whenever you wish to check a table to ensure it contains the correct data (or that it has loaded correctly). Each table object can be though of containing two things, a set of headers and a collection of table rows.

Table headers consist of a list of the table headers stored as a list on the headers attribute: >>> table = MyPage.ensure_table_is_loaded() >>> print table.headers [‘Food’, ‘Cost’, Quantity’]

Internally these are normally generated by a factory method that takes a Table web Element and returns a table object. This tries to work out what the table headers are by examining the table web element. If it can’t do so (perhaps because the table header cell doesn’t contain any text) it will substitute Col_0, Col_1 etc.

An individual table row can be accessed by index or by one of the convenience properties first_row and last_row: >>> assert table[0] == table.first_row

An individual table row consists of a TableRow object. The individual cells (`<td>...<.td>`) elements can be accessed by index or by key. The key is the relevant table header for that cell. Each cell is a <td> web element.

Example:

Food Cost $ | Quantity
Bananas 1 20
Apples 0.5 10
Pears 0.8 20
>>> table = MyPage.ensure_table_is_loaded()
>>> print table.headers
['Food', 'Cost', Quantity']
>>> print table[1]
{'Food': 'Apples', 'Cost': '0.5', 'Quantity: '10'}
>>> print table[1]['Food'].text
'Apples'
>>> print table[1][0].text
'Apples'
>>> print table.first_row
{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}
>>> print table.first_row['Food'].text
Bananas
>>> print table.last_row
{'Food': 'Pears', 'Cost': '0.8', 'Quantity: '20'}
>>> assert table.first_row['Food'].text == table[0][0].text
True

TableRow objects wrap OrderedDicts so have all the normal dictionary iter methods e.g.. values(), iteritems() etc. Comparisons and in methods work against the wrapped dict so work in the same way an OrderedDict would.

There are two other methods that can be used to retrieve table rows:

find_row_by_field and finds_row_by_field. The former is used to locate the first instance of a row where row[index] or row[header] matches a value. The latter returns a list of all rows that match:

>>> table.find_row_by_field('Food', 'Bananas')
{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}
>>> table.find_row_by_field('Food', 'Limes')
None
>>> table.find_rows_by_field(0, 'Bananas')
[{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}]
>>> table.find_rows_by_field('Food', 'Bananas')
[{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}]

There is a column method that returns a TableColumn object by its header value:

food_column = table.column('Food')
cost_column = table.column(1)

..Note:

an IndexError will be raised if the corresponding column is not found.

A table column is an immutable sequence with a header attribute.

>>> food_column.header
Food
>>> food_column
TableColumn('Food', ('Bananas', 'Apples', 'Pears')
>>> len(food_column)
3
>>> print food_column(1)
Apples
>>> 'Pears' in food_column
True
...Note:
When comparing against a TableColumn the header is ignored and all comparators are coerced to tuples so you can compare against lists etc.
>>> food_column = ['Bananas', 'Apples', 'Pears']
True
>>> shopping_list = TableColumn('Shopping List', ['Bananas', 'Apples', 'Pears'])
>>> food_column == shopping_list
True
>>> food_column == shopping_list and food_column.header == shopping_list.header
False

All sequence methods work in a similar way (i.e.. !=, >, <, >=, <=)

Page Objects

Most of the methods on page objects are reflections of methods defined on Selenium web driver and the Selenium documentation should be consulted for information on these.

The exceptions are the wait_for_element methods which correspond to the equivalent find_element method, reload and ensure_table_is_loaded. The latter is only present on page objects that sub class Page and contain a table (see defining a Page object below for details of how this works).

Aside from page specific methods the base Page class provides some methods to set up the data needed to load a page. Normally these are called by the __init__ method of classes that sub-class Page so you don’t have to call them directly. However there are occasionally times when you need to call them directly to set up the data for a page you will subsequently navigate to. Typically this occurs when you start on the main page before navigating elsewhere.

The methods are create_record and create_project.

create_record sets up an import record and file and optionally a canonical building and associated snapshot to go with them.

The import file and record are always created (in minimal form). To define any attributes on them pass in a dictionary to the import_record or import_file parameters.

To create a building set create_building to True and/or pass a dict to building.

create_project likewise creates a project and an associated canonical_building. You can supply a project name by passing name a string. Otherwise it will default to ‘test’. A canonical building instance can be provided to building. Otherwise the page objects self.canonical_building will be used (assuming it has been set up by create_record)

In order to retrieve a canonical_building you can use the get_canonical_building method. This will return self.canonical_building (or None) if not the id parameter is supplied. If the optional id parameter is used the canonical_building with that id is returned. This is useful in cases where the canonical_building was created by another page object.

Defining Page Objects.

In most cases the appropriate page object should have already be defined in pages.py so you can import it there, so you should never need to call Page directly (though its perfectly possible for one offs).

If you don’t find one its easy to define and add one to pages.py

Basic Example:

class Home(Page):
    def __init__(self, test_obj, use_url=None):
        if use_url:
            url = "/index.html"
        self.locator = Locator('NAME', 'my-button')
        super(Home, self).__init__(test_obj, locator, url=url)
        self.load_page()

This is all that is needed in simple cases. Most of it should be self explanatory. Be sure to call super before calling load_page. You need to always call the latter directly. Though you are free to add other, page specific methods, its typically to write an __init__ method and then rely on the methods defined by the Page base class.

self.locator is used to check that the page has been loaded correctly and must be defined. A Locator is a named tuple that consists of a strategy and a selector. Strategies are defined in page.py these correspond to Selenium waits.

Thus Locator(‘NAME’, ‘my-button’) is equivalent to wait_for_element_by_name(‘my-button’) which is like find_element_by_name(‘my-button’) and looks for a element whose name attribute is ‘my-button’ e.g. <a name=’my-button’ href=’/’>click me</a>

Where ever possible the Locator should be something that uniquely identifies the page. This is not always possible however. In these instances you can define a self.use_text attribute before calling super. This will provoke an additional check to ensure the page has loaded: The element identified by locator will also be checked to make sure it contains self.use_text (i.e. element .text == self.use_text).

Example with Table:

class Home(Page):
    def __init__(self, test_obj, use_url=None):
        if use_url:
            url = "/index.html"
        self.locator = Locator('NAME', 'my-button')
        self.table_locator = Locator('XPATH', '//table')
        super(Home, self).__init__(test_obj, locator, url=url)
        self.load_page()

If a page contains a table just add self.table_locator to give access to it. (Locator(‘XPATH’, ‘//table’) grabs the first table on the page and is often sufficient if there is not a better way of identifying it). Doing so causes the ensure_table_is_loaded method to be added to the Page object (Page actually defines __ensure_table_is_loaded, the double underscore causes name mangling so it can’t be accessed directly. Behind the scenes Page sets ensure_table_is_loaded = None as a class attribute and in its __init__ method checks to to see if self.table_locator is defined. If it is it sets self.ensure_table_is_loaded to __ensure_table_is_loaded. This is done so an error will be raised if you try to access ensure_table_is_loaded on a page with out a table (locator).

The Page class uses the table locator to retrieve a table web element, this is fed to a factory method that returns a Table object when ensure_table_is_loaded is called. Normally this is sufficient. While the factory method does a good job in most cases there is a lot of variation in how data tables are constructed so its not practical to have it attempt to cover all edge cases. In these cases you can override ensure_table_is_loaded. See BuildingListSettings in pages.py for an example.

Complex Example:

class DataMapping(Page):
    """
    Page object for the data mapping page

    dataset_id and create_import are mutually exclusive. dataset_id
    will take priority. The page will load directly (by url) if dataset_id
    or create_import are set. If import_record, import_record or building
    are supplied create_import will be set to True.

    :param: dataset_id: id of dataset (used in url)
    :param  create_import: create an import record before loading
    :param import_record: define additional attributes of the import record
    :param import_file: define additional attributes of the import file
    :param building: Add building if true, use dict for additional attributes

    :type: dataset_id: int
    :type: use_url: bool
    :type: create_import: bool
    :type: import_file: dict
    :type: import_record: dict
    :type: building: bool or dict
    """
    def __init__(self, test_obj, dataset_id=None, create_import=None,
                 import_record=None, import_file=None, building=None):
        locator = Locator('CLASS_NAME', 'mapping')
        # will cause ensure_table_is_loaded method to be added
        self.table_locator = Locator('CLASS_NAME', 'table')
        if import_record or import_file or building:
            create_import = True
        url = "app/#/data" if dataset_id or create_import else None

        super(DataMapping, self).__init__(
            test_obj, locator, url=url
        )

        # page set up
        if create_import and not dataset_id:
            create_building = True if building else False
            building = building if isinstance(building, dict) else None
            imports, canonical_building = self.create_record(
                create_building=create_building,
                import_record=import_record,
                import_file=import_file,
                building=building
            )
            if canonical_building:
                self.canonical_building = canonical_building
                self.building_id = self.canonical_building.id
            self.import_file = imports.import_file
            self.import_record = imports.import_record
            dataset_id = self.import_record.id
        if dataset_id:
            self.dataset_id = dataset_id
            self.url += "/{}".format(dataset_id)

        self.load_page()

This is a real example from pages.py and is about as complex as page objects need to get. Note that the create_record method is called after super but before load_page(). self.url is modified to add self.building_id here so that the page loads correctly.

base.py

This takes care of defining the base classes for use in tests.

Adding a new base class.

Add your class definition as normal, sub-classing `FunctionalLiveServerBaseTestCase` or one of the classes from derived from this.

Next add a factory function:

def myTestCaseFactory(browser):
    classname = get_classname('myTestCase', browser.name)
    return type(
        classname, (myTestCase, ),
        {'browser_type': browser}
    )

Then at the end of the file add a blank container dictionary and a call to your factory function in for loop:

MY_TEST_CLASSES = {}
for browser in BROWSERS:
    bname = browser.name
    MY_TEST_CLASSES[bname] = myTestCaseFactory(browser)

This will fill your container dictionary with Browser specific versions of your base class like this:

   {
       'Firefox': MyTestCaseFirefox,
       ...
   }

The container dictionary can then be imported in test_browser for use.

browser_definitions.py

This contains browser definitions and capabilities in order to set the right web driver on tests. They are used by the class factories in base.py and test generators in test_browser.py to generate browser specific versions of the test classes.

Browser definitions are listed in the BROWSERS list for easy import.

Adding a BrowserDefinition

A Browser definition is a named tuple that defines a browser for use in Test classes. e.g.:

myBrowser = BrowserDefinition(
    name = 'MyBrowserName',
    Capabilities = MyBrowserCapabilities,
    driver = MyBrowser.webdriver
)

or

myBrowser = BrowserDefinition(
    'MyBrowserName', MyBrowserCapabilities, MyBrowser.webdriver
)
Definitions:
  • name is a string, e.g. browser name (and version).
  • Capabilities is a dictionary that will be passed to the remote webdriver via Travis (which passes it to Sauce Labs) Describing it further is out of scope for this document. see: https://wiki.saucelabs.com/display/DOCS/Test+Configuration+and+Annotation
  • webdriver will used to power the tests if they are run locally. Normally this can just be MyBrowser.webdriver, but you can define any function and pass it in here. Note for both it must be func not func() or webdriver not webdriver()

A browser capabilities factory is provided for your convenience in base.py. This should ensure a valid browser capacity definition.

Depending on whether the tests are running locally or on Travis Capabilities or driver will be used.

Make sure to add your browser definition to BROWSERS (or equivalent) (and your capacity to BROWSER_CAPACITIES).

page.py

This defines the Page object base class, TableRow, TableColumn and supporting functionality.

pages.py

Page specific page objects. Import from here.

Adding a Page object subclass

See Page objects above

test_browser.py

The tests themselves live here.

The Gory Details

There is a lot of indirection and dynamic definition going on underneath the hood (compounded by the fact that there’s a lot of deep magic going on with unit tests in the first place). However I tried to write in such a way that it uses common idioms for things that will be changed frequently so mostly this can be ignored. Explanations for what is going on can be found below if you want, or need, to know.

The rationale for all this is easy testing across multiple browsers. As long as we wrap them in the right way we need only to write our base and test classes once and we will get a set of tests for each browser definition with out having to worry about the definitions, if a new definition is added it will automatically get picked up by all tests.

base.py details

This contains the base class definition `FunctionalLiveServerBaseTestCase` as well as other classes derived from it. The thing to note about this is that the setUp method detects the environment the tests are running in in the setUp method and uses this to add the correct browser instance to the class instance.

At the end of the file this list is looped over and the browser definition passed to a factory function. This takes the base class and returns a browser specific version of it so TestCaseFactory(browser) returns TestCaseFactoryBrowser which is added to a dictionary that can be imported elsewhere.

test_browsers.py details

This contains the actual tests themselves. It imports BROWSERS and the browser class dictionaries e.g. LOGGED_OUT_CLASSES from base.by.

The Test Classes defined here live inside a generator.

Each generator loops over BROWSERS and subclasses the appropriate base class from the browser class dictionary. It would be nice if we could yield this test class instance directly and pass it to the test runner. Unfortunately unittest expects a function from a generator. Actually things are a little more complicated than this. If you yield an object that’s not a function (i.e. everything else) it looks for the presence of a runTest method on it. If it find one it decides its a test and will call the object directly (not the runTest method) so yielding a class is like calling Class(), i.e. it instantiates the class but doesn’t call the test_methods.

To get around this the generator instantiates a copy of that class and passes it to get_tsts(). This takes care of calling setUpClass, which would not otherwise be run, then inspects the object for test method. When it finds one it wraps a call to that method in a function that takes care of invoking setUp and tearDown before, and after its run. Since it is now a function this can be safely yielded by the generator to be invoked by the test runner.

Base

Page

This module defines the functionality needed to create Page objects.

A Page object is a way of representing the page being tested separately from the unit test, and contains methods that the unit test can use to interact with that page, and the elements it contains.

A Page object needs to be supplied a web driver (browser instance) on initialization: this is done by passing a Test Case instance (that inherits from a class defined in base.py). This is done so Page objects can also utilize some of its other functionality and attributes, as well as the browser instance.

Note a Page object should not directly contain any functionality necessary to set up the page, e.g. database interactions. These methods remain with the Test Case classes defined in base.py. However they may be accessed by classes that subclass Page by calling the test_obj method so that subclasses can handle setting up a page. Since creating a building snapshot is so common there is a create_building method provided by Page.

Example:

Sub Classing Page

This is the preferred method and should be used any time a page will be used in multiple tests.

Defining the page object

class Home(Page):
def __init__(self, test_obj):
url = “index.html” locator = Locator(‘NAME’, ‘my-button’) super(Home, self).__init__(test_obj, locator, url=url) self.load_page()

Warning

a locator must be defined and passed to super(Class, self).__init__

Calling the page object in a test

::

from seed.functional.tests.browser_definitions import import BROWSERS from seed.functional.tests.base import LOGGED_IN_CLASSES from seed.functional.tests.pages import Home

def my_tests_generator():

for browser in BROWSERS:

class Tests((LOGGED_OUT_CLASSES[browser.name]):

def my_test(self):
home_page = Home(self) my_element = home_page.find_element_by_name(‘example’) assert my_element.text = ‘example text’
Example:

Sub Classing the Page Object for a page with a table

class Home(Page):
def __init__(self, test_obj):
url = “index.html” locator = Locator(‘NAME’, ‘my-button’) # will cause ensure_table_is_loaded method to be added self.table_locator = Locator(‘XPATH’, ‘//table’) super(Home, self).__init__(test_obj, locator, url=url) self.load_page()

Calling the page object in a test

::

from seed.functional.tests.browser_definitions import import BROWSERS from seed.functional.tests.base import LOGGED_IN_CLASSES from seed.functional.tests.pages import Home

def my_tests_generator():

for browser in BROWSERS:

class Tests((LOGGED_OUT_CLASSES[browser.name]):

def my_test(self):
home_page = Home(self) table = home.page.ensure_table_is_loaded() assert table[0][0].text = ‘example text’
Example:

Calling Page directly

::

from seed.functional.tests.browser_definitions import import BROWSERS from seed.functional.tests.base import LOGGED_IN_CLASSES from seed.functional.tests.page import Locator, Page

def my_tests_generator():

for browser in BROWSERS:

class Tests((LOGGED_OUT_CLASSES[browser.name]):

def my_test(self):

url = “{}/home.html”.format(self.live_server_url) my_locator = Locator(‘NAME’, ‘my-button’) my_page = Page(self, my_locator, url=url) my_page.load_page() my_element = my_page.find_element_by_name(‘example’) assert my_element.text = ‘example text’

# loads the next page, so we don’t need a url my_element.click() my_other_locator = Locator(‘IF’, ‘my-id’) my_other_page = Page(self, my_locator) my_other_element = my_other_page.find_element_by_id(‘example’) assert my_other_element.text = ‘example text’

:author Paul Munday<paul@paulmunday.net>

class seed.functional.tests.page.Imports(import_file, import_record)
import_file

Alias for field number 0

import_record

Alias for field number 1

class seed.functional.tests.page.Locator(strategy, search_term)
search_term

Alias for field number 1

strategy

Alias for field number 0

class seed.functional.tests.page.Organization(org, sub_orgs)
org

Alias for field number 0

sub_orgs

Alias for field number 1

class seed.functional.tests.page.Page(test_obj, locator, url=None, timeout=None, use_text=None)

A web page under test, :param: locator: Locator object used to identify page :param: url: if True load by url, if False assume page is loaded :param: timeout: time out in seconds :param: use_text: if a string is supplied its value will be checked on locator

Tpye:locator: Locator object
Type:url: Bool or string, if string append to self.url.
Type:timeout: int or float
Type:use_text: string
create_project(name=None, building=None)

Create a project (and project building). If no building is supplied self.canonical_building will be used if present.

Param:

name: project name

Param:
building:canonical building
Type:

name: string

Type:

building: CanonicalBuilding instance

create_record(create_building=False, import_record=None, import_file=None, building=None)

Set up an import/building snapshot in the db.

Pass dictionaries to import_file, import_record and building to set additional attributes. e.g. import_record= {‘name’: ‘Name’} can be used to set the import record/data set name.

As ImportFile.file is a django.db.models.FileField field it can be tricky to manipulate/mock. Use mock_file_factory from base.py to generate a mock_file and add it to the import_file dict. This will be added after the record is created and can be used to set the file name etc.

Parameters:
  • create_building – create a building snapshot
  • import_file – define additional attributes of the import file
  • building – define additional attributes of the building snapshot
Param:

import_record: define additional attributes of the import record

Type:

create_building: Bool

Type:

import_record: dict

Type:

import_file: dict

Type:

building: dict

generate_buildings(num, import_file=None, building_details=None)

Create multiple buildings.

get_canonical_building(id=None)

Get canonical building, by id or associated with Page, (self.canonical_building)

Param:id: building id/CanonicalBuilding primary key
Type:id: int
Returns:CanonicalBuilding instance or None
reload()

Reload the page if the browser has navigated away. i.e. another Page instance has been created.

Warning

This method does not navigate back to the page. You will need to do so yourself in your test. e.g. by locating and clicking on a suitable link.

It is not possible to reliably use self.browser.get(self.url) as get() watches for the page onload event and this does not happen on many pages as page loads are effectively overwritten as an ajax call once the page has been loaded the first time.

wait_for_element(strategy, search, timeout=None)

Get a page element, allowing time for the page to load.

:returns WebElement.

wait_for_element_by_class_name(selector, timeout=15)

Get a page element by class name, allowing time for the page to load.

:returns WebElement.

wait_for_element_by_css_selector(selector, timeout=15)

Get a page element by css, allowing time for the page to load.

:returns WebElement.

wait_for_element_by_id(selector, timeout=15)

Get a page element by id, allowing time for the page to load.

:returns WebElement.

Get a page element by link_test, allowing time for the page to load.

:returns WebElement.

Get a page element by partial_link_test, allowing time for the page to load.

:returns WebElement.

wait_for_element_by_tag_name(selector, timeout=15)

Get a page element by tag name, allowing time for the page to load.

:returns WebElement.

wait_for_element_by_xpath(selector, timeout=15)

Get a page element by xpath, allowing time for the page to load.

:returns WebElement.

class seed.functional.tests.page.Table(headers, rows, safe=True)

Provides a convenient way to query/compare a table.

Param:headers: Table headers
Param:rows: Table rows
Param:safe: if True raise an exception if number of columns and headers differs
Type:headers: Sequence (list, tuple etc)
Type:rows: Sequence of sequences/TableRows/OrderedDicts
Type:safe: bool
Example:
Food Cost $ | Quantity
Bananas 1 20
Apples 0.5 10
Pears 0.8 20
>>> table = MyPage.ensure_table_is_loaded()
>>> print table.headers
['Food', 'Cost', Quantity']
>>> print table[1]
{'Food': 'Apples', 'Cost': '0.5', 'Quantity: '10'}
>>> print table[1]['Food'].text
'Apples'
>>> print table[1][0].text
'Apples'
>>> print table.first_row
{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}
>>> print table.first_row['Food'].text
Bananas
>>> print table.last_row
{'Food': 'Pears', 'Cost': '0.8', 'Quantity: '20'}
>>> print table.column(0)
('Food', ['Bananas', 'Apples', 'Pears'])
>>> print table.column(0).header_text
Food
>>> print table.column(0)[0].text
Bananas
>>> print table.column('Food')
('Food', ['Bananas', 'Apples', 'Pears'])
>>> print len(table)
3
>>> print len(table.first_row)
3
>>> table.first_row.values()
['Bananas', '1', '20']
>>> table.first_row.keys()
['Food', 'Cost', Quantity']
>>> for key, val in table.first_row.items():
    >>>    print "{} = {}".format(key, val)
Food = Bananas etc.
>>> table.find_row_by_field('Food', 'Bananas')
{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}
>>> table.find_row_by_field('Food', 'Limes')
Noneed/functional/tests/page.py
>>> table.find_rows_by_field(0, 'Bananas')
[{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}]
>>> table.find_rows_by_field('Food', 'Bananas')
[{'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}]
>>> expected = Table(
    ['Food', 'Cost', Quantity'],
    [['Bananas', '1', '20'], ['Pears, 0.8, 20]]
)
>>> print expected.first_row
    {'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'}
>>> expected.first_row == table.first_row
True
>>> expected == table
False
>>> table.first_row in expected
True
>>> table[1] in expected
False
>>> {'Food': 'Apples', 'Cost': '0.5', 'Quantity': '10'} in table
True
>>> [row for row in table]
[TableRow({'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'})...]

Instantiating: Typically you use the page objects ensure_table_is_loaded() method to load the page:: >>> table = MyPage.ensure_table_is_loaded()

This does the equivalent of this:: >>> table_web_element = year_ending = page.find_element_by_xpath(“//table”) >>> table = table_factory(table_web_element, webdriver)

You can also instantiate it directly, if, for instance you have a lot of values you want to test against:

from collections import OrderedDict
table = Table(
    ['Food', 'Cost', Quantity'],
    OrderedDict({'Food': 'Bananas', 'Cost': '1', 'Quantity: '20'})
)

Tuples or lists can be used. OK:

Table(
["col 1", "col 2", "col 3"],
[
    [1, 2, 3],
    [4, 5, 6]
]
Table(
    ["a", "b]",
    [
        TableRow(OrderedDict({'a': 0, 'b': 1})),
        OrderedDict({'a': 2, 'b': 3}),
        TableRow([('a', 4), ('b':5)]
    ]
)

Warning

This is probably not what you want:

Table(
    ["a", "b]",
    [[('a', 1), ('b':2)], [('a', 4), ('b':5)]]
)

Equivalent to:

Table(
    ["a", "b]",
    [
        TableRow(OrderedDict({
            'a': [('a', 1), ('b':2)]
            'b': [('a', 4), ('b':5)]
        })
    ]
)
column(idx)

Return a table column corresponding to idx header,

A table column is always returned if a column exists, but its values are set to None where row[idx] is not present.

An index error will be raised if column corresponding to idx is not present.

find_row_by_field(idx, value)

Returns first row where row[idx].text = value, or None

Param:idx: index of column/cell or column name
Param:value: return rows whose text matches this value
Type:idx: int/string
Type:value: string
find_rows_by_field(idx, value)

Returns all rows where row[idx].text = value, or an empty list

Param:idx: index of column/cell or column name
Param:value: return rows whose text matches this value
Type:idx: int/string
Type:value: string
class seed.functional.tests.page.TableColumn(header, elements=None)

An immutable sequence to represent a table column. The table header is stored in the table attribute.

Comparisons: Header is not compared with comparisons. All comparisons are coerced to tuples so you can compare against any seq.

Examples:: >>> col0 = TableColumn(‘cost’, [1,2,3]) >>> col0.header cost >>> 1 in col0 True >>> col0 == (1, 2, 3) True >>> col0 == [1, 2, 3] True >>> col1 = TableColumn(‘price’, [1,2,3]) >>> col0 == col1 True >>> col0 == col1 and coll0.header == col1.header False

class seed.functional.tests.page.TableRow(constructor, **kwargs)

TableRow is an immutable variant of an OrderedDict whose values can also be accessed by index (i.e. as if they were a list) when an int is supplied.

In order to achieve this all key values are coerced to strings. While it will take numbers as keys in order to access them using [key] you must supply key as a string. However .get() performs the same coercion so e.g. .get(1) will work with the caveat that an error will not be raised if the key is not found.

In order to prevent accidental clobbering of keys a KeyError will be raised if the number of keys is not the same as that supplied. i.e. TableRow({‘1’, 1, 1: 2}) will throw an error. However TableRow({‘a’: 1, ‘a’: 2}) will also throw an error, where creating a dict of the same would result in {‘a’: 2}.

Example:
>>> tr = TableRow(OrderedDict({'a': 0, 'b': 1}))
>>> tr['a']
0
>>> tr[0]
0
>>> tr
TableRow({'a': 0, 'b': 1})
>>> tr = TableRow([(1, 2), ('2', 3)])
>>> tr['2']
3
>>> tr[0]
2
>>> tr[1]
3
>>> tr.get(1)
2
>>> tr.get(3)
None
>>> tr = TableRow({'a': 0, 'a': 1})
KeyError
>>> tr = TableRow([(2, 2), ('2', 3)])
KeyError
seed.functional.tests.page.table_factory(table)

Constructs a Table instance from a table web element.

It can be difficult to locate elements in tables in selenium, unless you have a name or id. Typically you would have to locate e.g. a table cell by using an XPath. These can be long and difficult to figure out since you need to locate row and column by a number corresponding to their position and this is difficult to figure out since the table structure can vary.

This avoids this issue by taking a table Web Element and then examining if for table headers. Based on what it finds in iterates through the table rows and converts these into ordered dictionaries where the key is the value of the table header and the value is the Web Element that represents the corresponding table cell.Where possible it tries to determine the table_header by examining the inner text contained in a header cell. If it is unable to do so it will instead use Col_0 etc

Returns:instance of Table

Pages

This module defines Page objects representing view in SEED.

Example:

Defining a page object

Class definition:

class Home(Page):
    def __init__(self, test_obj):
        url = "/index.html"
        self.locator = Locator('NAME', 'my-button')
        super(Home, self).__init__(test_obj, locator, url=url)
    self.load_page()

Warning

a locator must be defined and passed to super(Class, self)__init__

Calling the page object in a test:

from seed.functional.tests.browser_definitions import import BROWSERS
from seed.functional.tests.base import LOGGED_IN_CLASSES
from seed.functional.tests.pages import Home


def my_tests_generator():
    for browser in BROWSERS:

        class Tests((LOGGED_OUT_CLASSES[browser.name]):

        def my_test(self):
            home_page = Home(self)
            my_element = home_page.get_element_by_name('example')
            assert my_element.text = 'example text'
Example:

Defining the Page Object for a page with a table

Class definition:

class Home(Page):
    def __init__(self, test_obj):
        url = "index.html"
        locator = Locator('NAME', 'my-button')
        # will cause ensure_table_is_loaded method to be added
        self.table_locator = Locator('XPATH', '//table')
        super(Home, self).__init__(test_obj, locator, url=url)
        self.load_page()

Calling the table object in a test:

from seed.functional.tests.browser_definitions import import BROWSERS
from seed.functional.tests.base import LOGGED_IN_CLASSES
from seed.functional.tests.pages import Home


def my_tests_generator():
    for browser in BROWSERS:

        class Tests((LOGGED_OUT_CLASSES[browser.name]):

        def my_test(self):
            home_page = Home(self)
            table = home.page.ensure_table_is_loaded()
            assert table[0][0].text = 'example text'
author:Paul Munday<paul@paulmunday.net>
class seed.functional.tests.pages.AccountsPage(test_obj, use_url=None, create_import=None, import_file=None, building=None, sub_org=None)

Bases: seed.functional.tests.page.Page

Page object for the Organizations/account page. app/#/accounts

An import record and building will be created if url is True. An accounts page object will have an org attribute. This is self.org from the test case. It may also have a ‘sub_org’ attribute. It is only present if a sub_org exists. The sub_org will either be test_obj.sub_org if it already exists when the object is initialized, otherwise the first sub_org created.

Param:

use_url: if True load by url, if False assume page is loaded

Create_import:

create an Import record (with building) if true

Parameters:
  • import_file – define additional attributes of the import file
  • building – define additional attributes of the building snapshot
Sub_org:

create a SubOrganization if True or string(name) or list (names).

Type:

url: bool ,None.

Type:

create_import: bool, None

Type:

import_file: dict

Type:

building: dict

Type:

sub_org: bool, string

create_sub_org(name)

Create a sub organization

get_managed_org_tables()

Return a list of managed(owned) organizations.

Each organization is a named tuple of type Organization. Organization.org is a TableRow for the organization. Organization.sub_orgs is a list containing sub-organizations if any.

If there are sub orgs/the org is parent org, or capable of being so, the first row of sub orgs is a TableRow whose first cell is the (header) text ‘Sub-Organizations’, and whose second contains the ‘Create new sub-organization’ link.

Then any sub orgs will be listed. All TableRows have two parts: the first cell contains the organization name (or ‘Sub-Organizations’) the second the controls i.e. links to ‘Settings’ and ‘Members’ if its a sub/child organization, ‘Settings’, ‘Sharing’, ‘Data’, ‘Cleansing’, ‘Sub-Organizations’, ‘Members’ if it’s a parent organization.

The keys are ‘ORGANIZATION’ and ‘Col_1’ respectively.

get_member_orgs_table()

Return a the table of organizations the user belongs to.

class seed.functional.tests.pages.BuildingInfo(test_obj, building_id=None, create_building=None, create_project=None, import_file=None)

Bases: seed.functional.tests.page.Page

Page object for the building details page.

The page will load directly(by url) if create_record or a building_id is set.

Pass a dictionary to create_building to define additional attributes of building record/snapshot.

building_id and create_building are mutually exclusive. building_id will take priority. If create project is true and there is a building (i.e. create_building or building id is True), it will be added to the project.

Param:building_id: Canonical building id, page will load by url if set.
Param:create_building: Create a building when instantiating.
Param:create_project: Create a project . Supply string for project name.
Parameters:import_file – define additional attributes of the import file.
Type:building_id: int
Type:create_building: bool, None or dict
Type:create_project: bool, string, None
Type:import_file: dict
class seed.functional.tests.pages.BuildingLabels(test_obj, use_url=None)

Bases: seed.functional.tests.page.Page

Page object for the building reports page.

Param:use_url: load page directly by url
Type:use_url: bool
class seed.functional.tests.pages.BuildingListSettings(test_obj, use_url=None)

Bases: seed.functional.tests.page.Page

Page object for the building list settings page.

Param:use_url: load page directly by url
Type:use_url: bool
ensure_table_is_loaded()

Page uses stacked table to get fixed header, so needs own method.

class seed.functional.tests.pages.BuildingProjects(test_obj, id=None)

Bases: seed.functional.tests.page.Page

Page object for the building reports page.

Param:id: building_id, page will load directly(by url) if supplied.
Type:id: int
class seed.functional.tests.pages.BuildingReports(test_obj, use_url=None)

Bases: seed.functional.tests.page.Page

Page object for the building reports page.

Param:use_url: load page directly by url
Type:use_url: bool
class seed.functional.tests.pages.BuildingsList(test_obj, url=None, import_file=None, building=None, num_buildings=0)

Bases: seed.functional.tests.page.Page

Page object for the buildings list page.

An import record and building will be created if url is True.

Param:

url: if True load by url, if False assume page is loaded

Parameters:
  • import_file – define additional attributes of the import file
  • building – define additional attributes of the building snapshot
Num_buildings:

number of additional buildings to create (assumes use_url)

Type:

url: bool or string, if string append to self.url.

Type:

import_file: dict

Type:

building: dict

Type:

num_buildings: int

class seed.functional.tests.pages.DataMapping(test_obj, dataset_id=None, create_import=None, import_record=None, import_file=None, building=None)

Bases: seed.functional.tests.page.Page

Page object for the data mapping page

dataset_id and create_import are mutually exclusive. dataset_id will take priority. The page will load directly (by url) if dataset_id or create_import are set. If import_record, import_record or building are supplied create_import will be set to True.

Param:

dataset_id: id of dataset (used in url)

Parameters:
  • create_import – create an import record before loading
  • import_record – define additional attributes of the import record
  • import_file – define additional attributes of the import file
  • building – Add building if true, use dict for additional attributes
Type:

dataset_id: int

Type:

use_url: bool

Type:

create_import: bool

Type:

import_file: dict

Type:

import_record: dict

Type:

building: bool or dict

class seed.functional.tests.pages.DataSetInfo(test_obj, dataset_id=None, create_import=None, import_record=None, import_file=None, building=None)

Bases: seed.functional.tests.page.Page

Page object for the data set info page

dataset_id and create_import are mutually exclusive. dataset_id will take priority. The page will load directly (by url) if dataset_id or create_import are set. If import_record, import_record or building are supplied create_import will be set to True.

Param:

dataset_id: id of dataset (used in url)

Parameters:
  • create_import – create an import record before loading
  • import_record – define additional attributes of the import record
  • import_file – define additional attributes of the import file
  • building – Add building if true, use dict for additional attributes
Type:

dataset_id: int

Type:

use_url: bool

Type:

create_import: bool

Type:

import_file: dict

Type:

import_record: dict

Type:

building: bool or dict

class seed.functional.tests.pages.DataSetsList(test_obj, create_import=None, import_file=None, building=None)

Bases: seed.functional.tests.page.Page

Page object for the data sets list page.

The page will load directly (by url) if create_import is set.

Parameters:
  • create_import – create an import record before loading
  • import_file – define additional attributes of the import file
  • building – Add building if true, use dict for additional attributes
Type:

use_url: bool

Type:

create_import: bool

Type:

import_file: dict

Type:

building: bool or dict

class seed.functional.tests.pages.LandingPage(test_obj, use_url=None)

Bases: seed.functional.tests.page.Page

Page object for the Landing (Front/Home) Page.

Param:use_url: if True load by url, if False assume page is loaded
Type:use_url: bool
class seed.functional.tests.pages.MainPage(test_obj, use_url=None)

Bases: seed.functional.tests.page.Page

Page object for the Main Page. /app/#/

Param:use_url: if True load by url, if False assume page is loaded
Type:use_url: bool
class seed.functional.tests.pages.ProfilePage(test_obj, use_url=None, section=None)

Bases: seed.functional.tests.page.Page

Page object for the Profile Page. /app/#/profile

Param:use_url: if True load by url, if False assume page is loaded
Param:section: Which tab will be loaded. Default = profile
Type:use_url: bool.
Type:section: string one of profile/security/developer, case insensitive.
get_api_key_table()

Return API Key table.

reload(section=None)

Set section(use_text) before reloading. :param: section: Which tab will be loaded. Default = previous.

Type:section: None/string one of profile/security/developer.
class seed.functional.tests.pages.ProjectBuildingInfo(test_obj, use_url=False, name=None, building_id=None, create_building=None, create_project=None, import_file=None)

Bases: seed.functional.tests.page.Page

Page object for the Project building information page.

This is largely the same as the BuildingInfo page

The page will load directly(by url) if create_project or use_url is True.

If use_url is set you must supply name and building id. If use_url is not set you must supply name.

Pass a dictionary to create_building to define additional attributes of building record/snapshot.

building_id and create_building are mutually exclusive. building_id will take priority. If create project is true a building will be added to the project. Therefore either create_building must be True or building_id must be set.

Param:use_url: if True load by url, if False assume page is loaded
Param:name: Project name.
Param:building_id: Canonical building id
Param:create_building: Create a building when instantiating.
Param:create_project: Create a project.
Parameters:import_file – define additional attributes of the import file.
Type:use_url: bool
Type:name: str
Type:building_id: int
Type:create_building: bool, None or dict
Type:create_project: bool, None
Type:import_file: dict
class seed.functional.tests.pages.ProjectPage(test_obj, name=None, building_id=None, create_building=None, create_project=None, import_file=None)

Bases: seed.functional.tests.page.Page

Page object for the Projects Page. /app/#/projects

Pass a dictionary to create_building to define additional attributes of building record/snapshot.

building_id and create_building are mutually exclusive. building_id will take priority. If create project is true and there is a building (i.e. create_building or building id is True), it will be added to the project.

Param:name: Project name. If set will load by url appending name to url
Param:building_id: Canonical building id to add to project.
Param:create_building: Create a building when instantiating.
Param:create_project: Create a project.
Parameters:import_file – define additional attributes of the import file.
Type:name: str
Type:building_id: int
Type:create_building: bool, None or dict
Type:create_project: bool, None
Type:import_file: dict
class seed.functional.tests.pages.ProjectsList(test_obj, use_url=None, building_id=None, create_building=None, create_project=None, import_file=None)

Bases: seed.functional.tests.page.Page

Page object for the Projects Page. /app/#/projects

Param:use_url: if True load by url, if False assume page is loaded
Param:building_id: Canonical building id to add to project.
Param:create_building: Create a building when instantiating.
Param:create_project: Create a project. Supply string to set name
Parameters:import_file – define additional attributes of the import file.
Type:use_url: bool
Type:building_id: int
Type:create_building: bool, None or dict
Type:create_project: bool, None
Type:import_file: dict