Demo Autotests Project
pomcorn
package for PyPI
web site.You can get a demo project from package repository.
Setup
Environment
The simplest way to configure a proper Python version and virtual environment is using pyenv and poetry.
Prepare python interpreter:
$ pyenv install 3.12
$ pyenv shell $(pyenv latest 3.12)
Install dependencies
$ pip install -U poetry
$ poetry config virtualenvs.in-project true && poetry install --only main,demo && poetry shell
Running Autotests
To run tests, use invoke command pytest
:
$ inv pytest.run
About the project
This project is a mini autotesting system for the PyPI website. It implements the basic structure of pages and tests according to Page Object Model pattern.
The project structure looks like this
(__init__.py
files were skipped to simplify the structure):
│ demo/ ├── pages/ │ ├── base/ │ │ ├── base_components.py │ │ └── base_page.py │ ├── common/ │ │ ├── navigation_bar.py │ │ └── search.py │ ├── search_page/ │ │ ├── components/ │ │ │ ├── package_list.py │ │ │ └── package.py │ │ └── search_page.py │ ├── help_page.py │ ├── index_page.py │ └── package_details_page.py ├── tests │ ├── test_logo.py │ └── test_search.py └── conftest.py
Pages
This folder contains the page and component classes required to represent PyPI web pages. These classes contain web page interaction logic to make tests free of that implementation.
Base Folder
Basic classes for PyPI pages and components are implemented here.
Base Components
from typing import TypeAlias
from pomcorn import Component
from .base_page import PyPIPage
# In order not to specify generics every time, it's comfy to create your own
# type alias for `Component`.
PyPIComponent: TypeAlias = Component[PyPIPage]
Base Page
Note
The check_page_is_loaded
method and the APP_ROOT
attribute require special attention
here.
from __future__ import annotations
from typing import TYPE_CHECKING
from selenium.webdriver.remote.webdriver import WebDriver
from pomcorn import Page, locators
if TYPE_CHECKING:
from demo.pages import IndexPage
from demo.pages.common import Navbar
class PyPIPage(Page):
"""Base representation of the page for PyPI.
This is the base page for all following pages, so here we have to implement
only properties and methods common to all pages of application.
"""
# Be sure to redefine this attribute:
# specify the base domain of your app here.
APP_ROOT = "https://pypi.org/"
def __init__(
self,
webdriver: WebDriver,
*,
app_root: str | None = None,
# Next arguments have default values, so you can delete/specify them.
wait_timeout: int = 5,
poll_frequency: float = 0.01,
):
super().__init__(
webdriver,
app_root=app_root,
wait_timeout=wait_timeout,
poll_frequency=poll_frequency,
)
# The Logo will be on all the pages of the application so we initialize
# it in base page class.
self.logo = self.init_element(
# The ``locator=`` keyword is optional here, but we recommend using
# it to be consistent with the method of the same name in
# ``Component``. Same with ``init_elements``.
locator=locators.ClassLocator("site-header__logo"),
)
# We recommend adding components to the page as properties, because it
# helps us to run `waits_until_visible` method every time this component is
# accessed. But if you need to perform some actions before manipulating
# this component (e.g. clicking, hovering, etc.), it's better to create
# opening methods on page (like `open_navbar`).
@property
def navbar(self) -> Navbar:
"""Get a component for working with the page navigation panel."""
from demo.pages.common import Navbar
return Navbar(self)
# Some pages can be slow to load and cause problems checking for unloaded
# items. To be sure the page is loaded, this method should return the
# result of checking for the slowest parts of the page.
def check_page_is_loaded(self) -> bool:
"""Return the result of checking that the page is loaded.
Check that `main` tag is displayed.
"""
# Be careful with the elements you use to check page load. If you only
# use them to check loading, it's better to initiate them directly in
# this method. Otherwise, it is better to define them as page
# properties or initiate them in the `__init__` method above the
# `super().__init__` call. This is necessary because the
# `wait_until_loaded` method will be called in `super().__init__`, and
# it depends on `check_page_is_loaded`.
return self.init_element(
locator=locators.TagNameLocator("main"),
).is_displayed
def click_on_logo(self) -> IndexPage:
"""Click on the logo and redirect to `IndexPage`."""
from demo.pages import IndexPage
self.logo.click()
return IndexPage(self.webdriver)
Common
This folder contains components common to multiple pages.
Search component
This class represents a search field that can be placed on multiple pages.
![]() |
![]() |
from __future__ import annotations
from typing import TYPE_CHECKING
from selenium.webdriver.common.keys import Keys
from demo.pages import PyPIComponent
from pomcorn import locators
if TYPE_CHECKING:
from demo.pages.search_page import SearchPage
class Search(PyPIComponent):
"""Component representing the search input field."""
# If you are not going to write anything in ``__init__`` and only want
# to set up ``base_locator``, you can specify it as a class attribute
base_locator = locators.IdLocator("search")
def find(self, text: str) -> SearchPage:
"""Paste the text into the search field and send `Enter` key.
Redirect to `SearchPage` and return its instance.
"""
from demo.pages.search_page import SearchPage
self.body.fill(text)
self.body.send_keys(Keys.ENTER)
return SearchPage(self.webdriver)
Search Page Folder
Search Page Components
Because a number of additional components needed to be created to implement the search page, a separate folder was created for this page. This is where the page itself and its dependent components are stored.
Package List
This class represents a list of found packages on the PyPI search page.

from demo.pages import PyPIPage
from pomcorn import ListComponent, locators
from .package import Package
class PackageList(ListComponent[Package, PyPIPage]):
"""Represent the list of search results on `SearchPage`."""
# By default `ListComponent` have `item_class` attribute with stored first
# Generic variable (Package in current case). This attribute is responsible
# for the class that will be used for list items.
base_locator = locators.PropertyLocator(
prop="aria-label",
value="Search results",
)
# Set up ``relative_item_locator`` or ``item_locator`` is required.
# Use ``relative_item_locator`` - if you want locator nested within
# ``base_locator``, ``item_locator`` - otherwise."
# You also may override ``base_item_locator`` property.
relative_item_locator = locators.ClassLocator(
class_name="package-snippet",
container="a",
)
Package
This class represents one found package listed on the PyPI search page.

from __future__ import annotations
from typing import TYPE_CHECKING
from demo.pages import PyPIComponent
from pomcorn import locators
if TYPE_CHECKING:
from demo.pages import PackageDetailsPage
class Package(PyPIComponent):
"""Represent the single search result (package) on `SearchPage`."""
@property
def name(self) -> str:
"""Get the package name."""
return self.init_element(
relative_locator=locators.ClassLocator("package-snippet__name"),
).get_text()
def open(self) -> PackageDetailsPage:
"""Click on the package and open its details page."""
from demo.pages import PackageDetailsPage
# The property `body` is available because the package is descendant of
# `Component`. It allows us to interact with the body of the component
# and we can check that the package is clickable.
self.body.click()
return PackageDetailsPage(self.webdriver)
Search Page
This class represents the PyPI search page.

from __future__ import annotations
from selenium.webdriver.remote.webdriver import WebDriver
from demo.pages import IndexPage, PyPIPage
from .components import PackageList
class SearchPage(PyPIPage):
"""Representation of the page with search results."""
@classmethod
def open(
cls,
webdriver: WebDriver,
*,
app_root: str | None = None,
) -> SearchPage:
"""Open the search page."""
# Open `IndexPage` and search for an empty query.
# This will redirect us to the `SearchPage'.
return IndexPage.open(webdriver).search.find("")
@property
def results(self) -> PackageList:
"""Get the component for work with the found packages."""
return PackageList(self)
Help Page
This class represents the PyPI help page. The title
property is
implemented here to show how you can use page properties to implement the check_page_is_loaded
method.

from __future__ import annotations
from selenium.webdriver.remote.webdriver import WebDriver
from demo.pages.base import PyPIPage
from pomcorn import Element, locators
class HelpPage(PyPIPage):
"""Represent the help page."""
# Define element for title
title_element = Element(locators.ClassLocator("page-title"))
@classmethod
def open(
cls,
webdriver: WebDriver,
*,
app_root: str | None = None,
) -> HelpPage:
"""Open the help page via the index page."""
from demo.pages.index_page import IndexPage
# Reusing already implemented methods of opening a page instead of
# overriding `app_root` allows us to be independent from URK changes:
# we move from one page to another, interacting with the page as the
# user does.
return IndexPage.open(webdriver, app_root=app_root).navbar.open_help()
def check_page_is_loaded(self) -> bool:
"""Return the check result that the page is loaded.
Return whether help page title element are displayed or not.
"""
return self.title_element.is_displayed
Index Page
This class represents the PyPI start page. This page shows the use of the class Search component.

from selenium.webdriver.remote.webdriver import WebDriver
from demo.pages.base import PyPIPage
from demo.pages.common import Search
class IndexPage(PyPIPage):
"""Represent the index page."""
def __init__(
self,
webdriver: WebDriver,
*,
app_root: str | None = None,
wait_timeout: int = 5,
poll_frequency: float = 0.01,
):
super().__init__(
webdriver,
app_root=app_root,
wait_timeout=wait_timeout,
poll_frequency=poll_frequency,
)
@property
def search(self) -> Search:
"""Get the search component.
Allows to work with the search field at the center of the page.
"""
return Search(self)
Package Details Page
This class represents the PyPI package details page.

from __future__ import annotations
from selenium.webdriver.remote.webdriver import WebDriver
from demo.pages.base import PyPIPage
from pomcorn import locators
class PackageDetailsPage(PyPIPage):
"""Represent the package details page."""
@property
def header(self) -> str:
"""Get the header text."""
return self.init_element(
locator=locators.ClassLocator("package-header__name"),
).get_text()
@classmethod
def open(
cls,
webdriver: WebDriver,
package_name: str,
*,
app_root: str | None = None,
) -> PackageDetailsPage:
"""Search and open the package details page by its name."""
from demo.pages import IndexPage
search_page = IndexPage.open(
webdriver,
app_root=app_root,
).search.find(package_name)
return search_page.results.get_item_by_text(package_name).open()
Note
You don’t have to implement the check_page_is_loaded
page method if this property is set on
the base page and is appropriate for the current page.
Tests
This folder contains autotests that use pages prepared in fixtures to reproduce some scenarios of user interaction with the site.
Test Logo
from demo.pages import HelpPage
def test_logo(help_page: HelpPage):
"""Check that click on site logo redirect to index page."""
old_url = help_page.current_url
index_page = help_page.click_on_logo()
index_page.wait_until_url_changes(old_url)
assert help_page.current_url.endswith("pypi.org/")
Test Search
from demo.pages import IndexPage
def test_search(index_page: IndexPage):
"""Check that the search on the index page works correctly.
Check that the search query matches the expected number of packages. Also
check if a specific package is found and click on it to redirect to the
details page.
"""
# Search packages by `saritasa`
search_page = index_page.search.find("saritasa")
# Check that at least 4 packages are presented
assert search_page.results.count >= 4
# Get package by name
package_name = "pomcorn"
package = search_page.results.get_item_by_text(package_name)
assert package.name == package_name
package_details_page = package.open()
assert package_name in package_details_page.header
Conftest
Here are the implemented base fixtures for implemented PyPI pages. This is useful practice to avoid duplicating page opening calls in each test. Also, the webdriver fixture with a given window size (1920×1080) is implemented here.
from selenium import webdriver as selenium_webdriver
from selenium.webdriver.remote.webdriver import WebDriver
import pytest
from demo.pages import HelpPage, IndexPage, SearchPage
# You can implement your own logic to initialize a webdriver.
# An example of Chrome initialization is described below.
@pytest.fixture(scope="session")
def webdriver() -> WebDriver:
"""Initialize `Chrome` webdriver."""
options = selenium_webdriver.ChromeOptions()
# Set browser's language to English
prefs = {"intl.accept_languages": "en,en_U"}
options.add_experimental_option("prefs", prefs)
webdriver = selenium_webdriver.Chrome(options)
webdriver.set_window_size(1920, 1080)
return webdriver
@pytest.fixture
def index_page(webdriver: WebDriver) -> IndexPage:
"""Open index page of PyPI and return instance of it."""
return IndexPage.open(webdriver)
@pytest.fixture
def help_page(webdriver: WebDriver) -> HelpPage:
"""Open help page of PyPI and return instance of it."""
return HelpPage.open(webdriver)
@pytest.fixture
def results_page(webdriver: WebDriver) -> SearchPage:
"""Open search results page of PyPI and return instance of it."""
return SearchPage.open(webdriver)