Page Object Model for operability

One of the core feature of Manen is its implementation of the Page Object Model design pattern, which provides an interface between the core methods of Selenium WebDriver, and the workflows of your application. By using this design pattern, you shouldn’t have to re-implement functions to access DOM elements; you just have to describe your page structure by breaking it into multiple components. This will help to increase the readability and operability of your code.

This step-by-step guide shows how to use Manen for such purposes. We will use PyPI web page as playground for the exploration.


First, we need a WebDriver instance that will be the entrypoint for all our code. We will also define an helper function in charge of taking a screenshot of the current page, and display it inline.

[1]:
from tempfile import NamedTemporaryFile

from IPython.display import Image, display
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.selenium_manager import SeleniumManager
[2]:
def screenshot(driver: WebDriver):
    with NamedTemporaryFile(suffix='.png') as f:
        driver.save_screenshot(f.name)
        display(Image(f.name))
[3]:
selenium_manager = SeleniumManager()
paths = selenium_manager.binary_paths(["--browser", "chrome"])

service = Service(executable_path=paths["driver_path"])
options = Options()
options.add_argument("--window-size=1024,768")
driver = WebDriver(service=service)
[4]:
driver.get('https://pypi.org')

# Switch to the english page for results consistency
driver.add_cookie({'name': '_LOCALE_', 'value': 'en'})
driver.refresh()

screenshot(driver)
../_images/user_guide_page_object_model_5_0.png

DOM interactions

The main idea behind the page object model is to use simple Pythonic code to interact with your page. Python classes, with their attributes, will be used to describe an HTML page. Each class will inherit from a Manen Component, in order to symbolize the fact that they should be related to the DOM of the current page.

Each class attributes must be annotated with the type of DOM value you want to retrieve, as long as the selectors used to locate the elements. Manen provides some types to represent basic DOM elements; you can also use the type str (to extract the inner text of an element), int or datetime to attempt to convert the text to the given types. Note that you can use element or Selenium WebElement to return the raw Selenium web element.

[5]:
from typing import Annotated

from manen.page_object_model.types import href, input_value, src
from manen.page_object_model.config import CSS, XPath
from manen.page_object_model.component import Form, Page, Component


class NavigationBar(Component):
    class NavigationMenuItem(Component):
        label: Annotated[str, CSS("a.horizontal-menu__link")]
        link: Annotated[href, CSS("a.horizontal-menu__link")]

    logo: Annotated[src, CSS("a.site-header__logo img")]
    items: Annotated[list[NavigationMenuItem], CSS("ul li.horizontal-menu__item")]


class PyPIHomePage(Page):
    class SearchForm(Form):
        query: Annotated[input_value, CSS("input[name='q']")]

    navbar: Annotated[NavigationBar, CSS("header.site-header")]
    search: Annotated[SearchForm, CSS("form.search-form")]

A page is initialized using the driver instance.

[6]:
page = PyPIHomePage(driver)

You can now access each one of the DOM values as attributes of the page or component you created.

[7]:
print(page.navbar.logo)
https://pypi.org/static/images/logo-small.8998e9d1.svg

Each component provides a method model_dump to serialize all the attributes into a dictionary.

[8]:
page.navbar.model_dump()
[8]:
{'logo': 'https://pypi.org/static/images/logo-small.8998e9d1.svg',
 'items': [{'label': 'Help', 'link': 'https://pypi.org/help/'},
  {'label': 'Sponsors', 'link': 'https://pypi.org/sponsors/'},
  {'label': 'Log in', 'link': 'https://pypi.org/account/login/'},
  {'label': 'Register', 'link': 'https://pypi.org/account/register/'}]}

Filling the value of the input with the search query is done with an assignment.

[9]:
page.search.query = "selenium"
[10]:
screenshot(driver)
../_images/user_guide_page_object_model_16_0.png

Given that page.search inherit from the Form class, it has an additional method submit compared to other component, you can call to submit the query. Note that it can also be done with the following instructions:

from selenium.webdriver.common.keys import Keys

page.search.query += Keys.ENTER
[11]:
page.search.submit()

By doing so, we navigated to another page, with the results of the search.

[12]:
print('Current URL:', driver.current_url)
screenshot(driver)
Current URL: https://pypi.org/search/?q=selenium
../_images/user_guide_page_object_model_20_1.png

Let’s define another page class to describe the new HTML structure. Notice that we re-use the class NavigationBar given that this web component is also on the current page.

[13]:
from datetime import datetime, timedelta

import pytz
from manen.page_object_model.config import CSS, Attribute, DatetimeFormat


class SearchResultPage(Page):
    class Result(Component):
        name: Annotated[str, CSS("h3 span.package-snippet__name")]
        version: Annotated[str, CSS("h3 span.package-snippet__version")]
        link: Annotated[href, CSS("a.package-snippet")]
        description: Annotated[str, CSS("p.package-snippet__description")]
        release_datetime: Annotated[
            datetime,
            DatetimeFormat("%Y-%m-%dT%H:%M:%S%z"),
            Attribute("datetime"),
            CSS("span.package-snippet__created time"),
        ]

        @property
        def is_recent(self):
            return self.release_datetime >= pytz.utc.localize(datetime.now()) - timedelta(days=180)

        def print_summary(self):
            summary = f"{self.name} (v{self.version}) has been released on {self.release_datetime.date()}"
            if self.is_recent:
                summary += ", less than 180 days ago"
            print(summary)

    navbar: Annotated[NavigationBar, CSS("header.site-header")]

    nb_results: Annotated[
        str,
        XPath("//*[@id='content']//form/div[1]/div[1]/p/strong"),
    ]
    results: Annotated[
        list[Result],
        CSS("ul[aria-label='Search results'] li"),
    ]
[14]:
page = SearchResultPage(driver)
[15]:
print("Number of results for the query:", page.nb_results)
print("Number of results on the page:", len(page.results))
Number of results for the query: 2,687
Number of results on the page: 20

Same as before, we can access the elemens of the navigation bar.

[16]:
page.navbar.model_dump()
[16]:
{'logo': 'https://pypi.org/static/images/logo-small.8998e9d1.svg',
 'items': [{'label': 'Help', 'link': 'https://pypi.org/help/'},
  {'label': 'Sponsors', 'link': 'https://pypi.org/sponsors/'},
  {'label': 'Log in', 'link': 'https://pypi.org/account/login/'},
  {'label': 'Register', 'link': 'https://pypi.org/account/register/'}]}

Besides, all the search results of the page will be on each item of page.results.

[17]:
page.results[0].model_dump()
[17]:
{'name': 'selenium',
 'version': '4.25.0',
 'link': 'https://pypi.org/project/selenium/',
 'description': 'Official Python bindings for Selenium WebDriver',
 'release_datetime': datetime.datetime(2024, 9, 20, 15, 4, 46, tzinfo=datetime.timezone.utc)}

Note that model_dump doesn’t include custom properties in the dictionary.

We can then call the custom method print_summary we implemented for Result item.

[18]:
for result in page.results[:5]:
    result.print_summary()
selenium (v4.25.0) has been released on 2024-09-20, less than 180 days ago
selenium2 (v0.1.1) has been released on 2024-06-02, less than 180 days ago
ak_selenium (v0.1.9) has been released on 2024-09-08, less than 180 days ago
automated-selenium (v1.1.1) has been released on 2023-05-29
axe-selenium (v2.1.6) has been released on 2024-09-26, less than 180 days ago

List of available Manen types

Manen implements out of the box the current elements:

Type

Description

checkbox

Form element to interact with a checkbox through boolean assignment

date

Extract the inner text of an element, and parse it to a date

datetime

Extract the inner text of an element, and parse it to a datetime

element

Return the raw Selenium element

float

Convert the inner text of an element to a float

href

Extract the href attribute of an element

inner_html

Extract the inner HTML of an element

input_value

Form element to interact with an input

int

Convert the inner text of an element to an integer

outer_html

Extract the outer HTML of an element

src

Extract the src attribute of an element

str

Extract the inner text of an element

The list of types provided by Manen intends to grow to cover a maximum of use cases.

Advanced usage

The page object implementation is using internally the find function, which has additional arguments to customize how to retrieve elements from the DOM (see associated user guide for more details about that). In the same way, you can add options to each class attributes to enable this level of customization in your page model.

Waiting a given duration for element to appear is done with the Wait config, whereas Default is used to set a default value when the element behind a DOM value is not found.

[19]:
from manen.page_object_model.config import Wait

class SearchResultPage(Page):
    nb_results: Annotated[
        int,
        XPath("//*[@id='content']//form/div[1]/div[1]/p/strong"),
        Wait(5),
    ]

    dont_exist: Annotated[str | None, CSS('i-dont-exist')]

page = SearchResultPage(driver)
[20]:
assert page.dont_exist is None

Note that when using a default value, the type specified in the annotation should be coherent with the default value, with the restriction that only not-None type is allowed. For example, you can’t have something like Annotated[int | str, CSS('i-dont-exist'), Default('not here')].


You are now familiar with the page object model implementation of Manen! Don’t hesitate to open an issue if you have any remarks, concerns, or feature request!

[21]:
driver.quit()