Page Object Modeling with Python and Playwright
How to construct effective models for Playwright frameworks.
Microsoft’s Playwright quietly released in May of 2020. Written by a group of engineers who originally wrote the Puppeteer library, Playwright is intentionally similar to Puppeteer. While Microsoft has provided an example of a page object within Playwright’s documentation, the example is fairly simple and does not delve into the use of page properties or helper methods.
What is a Page Object?
A page object is an abstraction of a web page using a programming language. The intention is to represent all of the page within code so as to take action against specific elements. Page objects are routinely used in the field of test automation where a Quality Engineer creates objects and tests for the purpose of testing application user journeys.
This tutorial will focus on building a page object for the DemoQA Bookstore application login page. The login page features a username input, a password input, a login button, and a new user button.
Getting Started
In order to begin, we need to install our dependencies. The following packages should be installed:
- pytest
- pytest-playwright
- playwright
Simply install the above using pip (or the dependency manager of your choosing).
The pytest
library is included as it is the most popular test runner for Python. The pytest-playwright
package provides simple fixtures for use within tests, such as the page fixture which automates browser setup. It can also be configured to take screenshots upon failure which is helpful in debugging flakey or failing tests.
Abstracting Page Elements
Our first task is to create a class object that represents the login page which features a page
attribute when initialized. The page
attribute will be used when creating properties and methods.
The reason for building a class object is that it allows our code to remain organized while increasing reusability.
class Login:
def __init__(self, page):
self.page = page
Now that we have our Login class, we can begin adding properties that represent elements within the page. We use the @property
built-in decorator as it makes using getters and setters easier within Python.
Each property returns what Playwright calls an ElementHandle
. This is Playwright’s representation of a DOM element. Playwright ElementHandle
objects may be used by themselves or as arguments to other methods.
class Login:
def __init__(self, page):
self.page = page @property
def new_user_button(self):
return self.user_form.wait_for_selector("#newUser") @property
def password_field(self):
return self.user_form.wait_for_selector("#password") @property
def submit_button(self):
return self.user_form.wait_for_selector("#login") @property
def user_form(self):
return self.page.wait_for_selector("#userForm") @property
def username_field(self):
return self.user_form.wait_for_selector("#userName")
The property username_field
is called from within the user_form
. This is because the username field is a child element of the user form in the DOM. This is a good practice as it narrows the scope of the driver to a specific area rather than the entirety of the page.
Playwright’s wait_for_selector
method allows us to call elements safely as it will wait for the element to be visible within the DOM until a timeout limit has been reached. An error is raised if the selector is not found within the timeout limit.
The wait_for_selector
method may also be used to wait for a specific element state such as waiting until hidden. However, that is out of scope for this guide.
Writing Helper Methods
Page object properties may be used in the construction of helper methods. Helper methods should be constructed for workflows which are repeated often.
class Login: def submit_login_form(self, user):
self.username_field.fill(user["username"])
self.password_field.fill(user["password"])
self.submit_button.click() def navigate(self):
self.page.goto("https://www.demoqa.com/login")
The above methods allow for page navigation and form submission, both workflows which will be used frequently during Login page testing.
Writing and Running a Test
Now that we have a page object built with both properties and helper methods, we can begin writing a test for a user journey.
from pages import Loginuser = {
"username": "test",
"password": "password"
}
class TestLogin:
def test_valid_login(self, page):
login_page = Login(page)
profile_page = Profile(page) login_page.navigate()
login_page.submit_login_form(user) visible = profile_page.username_value_field.is_visible() assert visible and "profile" in page.url
In the above test, we initialized both our Login page and a Profile page (example not shown) which is used in the assertion of the test. We navigate to the login page, then input user information using the submit_login_form
helper method.
Once our form is submitted, we check to see whether an element on the Profile page is shown. We then use this check in our assertion as this element is shown after successful login.
To run this test, input one of the following commands to your terminal:
pytest
to run headlesslypytest --headful
to run in a headful state
Project Directory
Your directory should resemble the following upon completion of this tutorial:
pages
|__ __init__.py
|__ login.pytests
|__ test_login.py.gitignore
README.md
requirements.txt
Summary
Writing tests using Page Object Model is fairly quick and convenient. Using Python and Playwright, we can effortlessly abstract web pages into code while automatically waiting for elements.
Jonathan Thompson is a Senior Quality Engineer specializing in test automation. He currently resides in Raleigh, NC with his wife and a Goldendoodle named Winston. You can connect with him on LinkedIn, or follow him on either Twitter (@jacks_elsewhere) or Github (ThompsonJonM).