Skip to content
/ pageobj Public

Selenium Page Object in Python for human beings

Notifications You must be signed in to change notification settings

wei-y/pageobj

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Selenium Page Object in Python

Features of this package :

  • Page object pattern in Python: concise descriptive definition for pages and element selectors
  • High level data structure of elements: array, dictionary, template, table, etc. and building customized component hierarchically
  • Wrap Selenium for human being:
    • wait things to happen using with protocal
    • wait for elements to show before operating on it
    • return None in place of raising exception when element not found
    • flexible routing decorator to convert page object to proper classes
    • fall back to raw Selenium if required

Usage

Dependencies

This package depends nothing more than built-in packages of Python 3 and Selenium Python Binding. Setup Selenium local, server or grid as required and create PageObject from the WebDriver object.

Defining a page

A login page could be defined as following.

@pageconfig(default_by=By.CSS_SELECTOR)
class TestLoginPage(PageObject):
    user = PageElement('input[name="email"]')
    password = PageElement('input[name="password"]')
    login_button = PageElement('button[type="submit"]')

    # Suppose we have a module called `test.py` and a PageObject class `BasePage`
    # is defined in it When user logged in they will land on the BasePage
    @nextpage('test.BasePage')
    def login(self, user, password):
        self.user = user
        self.password = password
        with self.wait_page_loaded_after(timeout=10):
            self.login_button.click()

PageObject

A page is defined by creating a class inherited from PageObject and declare all interested elements in the class as class attributes. An element declaration is an instance of PageElement class with selector as parameter. When accessing the element, it will return a raw Selenium WebElement, a PageComonent or the text/value of the element based on parameters to the PageElement initiator.

Adding actions

Page actions are defined as normal class methods which will use elements declared previously. Elements are defined as descriptors and will be evaluated at runtime. Elements can also be set by using assignment statement. Acceptable type of Values is based on the type of the element.

If the action is leading to another page which is also defined as a PageObject, a decorator nextpage() can be used on the action to describe the package/module path string to the page class. The target page class can be statically coded as a string, or a dictionary that can be looked up at runtime by using the return value of the method as key, or it can be a function taking the returned value as input and returns the targe page class string.

Default page settings

Using decorator pageconfig() to the PageObject to define the default By of element selectors and default timeout when accessing PageElement

PageComponent

Some elements on the page can be organized togather as a small functional component that can be reused in different places.

The base Dashborad page can be defined as following. Notice that the navigator is defined as a PageComponent.

@pageconfig(default_by=By.LINK_TEXT)
class PageNavigator(PageComponent):
    bookings = PageElement('Bookings')

    @nextpage({
        'bookings': 'test.test.BookingsPage',
    })
    def nav(self, menu):
        with WaitAJAXAfter(self.page):
            getattr(self, menu).click()
        return menu

@pageconfig(default_by=By.TAG_NAME)
class BasePage(PageObject):
    navigator = PageElement('aside', component=PageNavigator)

After logging in, the dashboard is more complex. Here a class containing only the navigator is defined as the base of each different pages. The navigator is a relatively independent section thus it is defined as a component using PageComponent.

A PageComponent is similar to PageObject. It contains sub-elements the same way as PageObject. The difference is that all sub-element is defined in the context of the container component.

A PageComponent can be used in other PageObject or nest in other PageComponent by passing in the component parameter to PageElement definition.

Decorators applicable to PageObject are also applicable to PageComponent.

PageTable

The Booking page can be defined as following. Notice that the main table is defined as a PageTable.

@pageconfig(default_by=By.CSS_SELECTOR)
class BookingRow(PageComponent):
    tick = PageElement('td:nth-child(1) input[type="checkbox"]')
    number = PageElement('td:nth-child(2)')
    id_ = PageElement('td:nth-child(3)')
    reference = PageElement('td:nth-child(4)')
    customer = PageElement('td:nth-child(5)')
    module = PageElement('td:nth-child(6)')
    date = PageElement('td:nth-child(7)')
    total = PageElement('td:nth-child(8)')
    paid = PageElement('td:nth-child(9)')
    remaining = PageElement('td:nth-child(10)')
    status = PageElement('td:nth-child(11)')
    view_button = PageElement('td:nth-child(12) a:nth-child(1)')
    edit_button = PageElement('td:nth-child(12) a:nth-child(2)')
    delete_button = PageElement('td:nth-child(12) a:nth-child(3)')

    @nextpage('test.test.BasePage')
    def view(self):
        windows_count = len(self.page.window_handles)
        with WaitAJAXAfter(self.page):
            self.view_button.click()
        self.page.wait(lambda drv: len(drv.window_handles) > windows_count)
        self.page.window(-1)

    @nextpage('test.test.BasePage')
    def edit(self):
        with WaitAJAXAfter(self.page):
            self.edit_button.click()

    def delete(self):
        with WaitAJAXAfter(self.page):
            self.delete_button.click()
        self.page.alert().accept()

@tableconfig(
    row_locator=(By.CSS_SELECTOR, 'tbody tr'),
    row_component=BookingRow
)
class BookingTable(PageTable):
    pass

@pageconfig(default_by=By.LINK_TEXT)
class BookingsPage(BasePage):
    print_button = PageElement('Print')
    export_csv = PageElement('Export into CSV')
    booking_table = PageElement('#content table', by=By.CSS_SELECTOR, component=BookingTable)

Defining a PageTable

A PageTable is any table-like structure defined in the DOM. a <Table> could naturally be defined as a PageTable but any structure in a table-like way can do. The benefit of using PageTable is that it provides index access and query ability.

A table row is defined as a PageComponent with each column as a sub-element or component. Since it is a Component, action can be defined in table row as well.

Once a table row is defined, it then can be used to define a PageTable by specify the row selector and class in tableconfig decorator. Then, the defined table can be used as a component in a PageObject.

Accessing by index and querying

Table rows can be accessed by using index notation. Table can also be queried by providing filters to columns. The filter can be a string or a function returning Boolean.

row = page.booking_table[0]
result = page.booking_table.query(paid=False, total=lambda v: v>100)

Waiting

In both PageObject and PageComponent it can wait for things to happen. There are two kinds of waitings: a direct waiting and waiting after operation.

A direct waiting can be invoked by calling the wait() with a callable as parameter. The callable is the same as the parameter to WebDriverWait().until().

self.wait(expected_conditions.visibility_of_element_located((By.ID, 'user')))

Waiting after operation is called by using the with protocal.

with self.wait_page_loaded_after(timeout=10):
    self.login_button.click()

And that's it

The full example is in test.py. To run the test file, use the following command.

python -m unittest discover

About

Selenium Page Object in Python for human beings

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Languages