Mastering Playwright: How to Create Effective Page Objects

Nikolay Advolodkin
Geek Culture
Published in
6 min readJan 27, 2023

Test automation is a crucial aspect of software development, helping to ensure that the application behaves as expected and meets the needs of its users. One of the key elements of successful test automation is the use of Page Objects. This blog post will explore how to create effective Page Objects using Playwright, a popular Node.js library for automating web browsers. We will discuss the principles of good Page Objects, such as single responsibility, abstraction, and encapsulation. We will also show how these principles can be applied when working with Playwright. By the end of this post, you will have a solid understanding of how to create maintainable and reusable Page Objects with Playwright.

Page object standards

A good Page Object should follow the following standards:

  1. Single Responsibility Principle (SRP): Each page object should be responsible for a single page or a small section of a page. This helps to keep the code organized and maintainable.
  2. Abstraction: Page objects should abstract away the details of interacting with the page, such as locators and methods for interacting with elements. This makes the test code more readable and less brittle.
  3. Encapsulation: Page objects should encapsulate the state and behavior of the page, making it easy to reason about the state of the page and the actions that can be performed on it.
  4. Reusability: Page objects should be reusable across different tests, reducing the amount of duplicated code.
  5. Easy to understand: The naming of methods and variables should be self-explanatory, making the code’s purpose easy to understand.
  6. Separation of Concerns: The test code should focus on the high-level behavior of the page, and the Page Objects should handle the low-level details of interacting with the page.

Example of a Page Object using TypeScript and Playwright:

import { Page } from 'playwright';
export class LoginPage {
private page: Page;
constructor(page: Page) {
this.page = page;
}
async visit() {
await this.page.goto('https://your-login-page.com');
}
async login(credentials: {username: string, password: string}) {
await this.page.type('#username', credentials.username);
await this.page.type('#password', credentials.password);
await this.page.click('#submit');
}
async isLoggedIn() {
return await this.page.evaluate(() => {
return document.querySelector('#logout') !== null;
});
}
}

In this example, the LoginPage class is responsible for interacting with the login page. It has a visit() method for navigating to the login page, a login() method for logging in with a given set of credentials, and an isLoggedIn() method for checking if the user is logged in. The class abstracts away the details of interacting with the page, such as the locators for the username and password fields, making the test code more readable.

It’s a simple example, but it illustrates the main principles of a good Page Object. You can follow a similar pattern for other pages as well.

Using page objects in a test

Here’s an example of the usage in a test:

import { Page, launch } from 'playwright';
import { LoginPage } from './LoginPage';
describe('Login Page', () => {
let browser: any;
let page: Page;
let loginPage: LoginPage;
beforeEach(async () => {
browser = await launch({ headless: true });
page = await browser.newPage();
loginPage = new LoginPage(page);
await loginPage.visit();
});
afterEach(async () => {
await browser.close();
});
test('user can login', async () => {
await loginPage.login({username: 'testuser', password: 'password'});
const isLoggedIn = await loginPage.isLoggedIn();
expect(isLoggedIn).toBe(true);
});
test('user cannot login with invalid credentials', async () => {
await loginPage.login({username: 'testuser', password: 'invalidpassword'});
const isLoggedIn = await loginPage.isLoggedIn();
expect(isLoggedIn).toBe(false);
});
});

In this example, the describe block defines a test suite for the login page. The beforeEach block is called before each test in the suite, and it sets up the browser, creates a new page, and initializes an instance of the LoginPage class. The afterEachblock is called after each test, and it closes the browser.

The two test blocks define two tests for the login page. The first test checks that a user can log in with valid credentials, and the second checks that a user cannot log in with invalid credentials. In both tests, the loginPage object is used to call the login() and isLoggedIn() methods and the results are checked using the expect function.

This example shows how the Page Object pattern can make the test code more readable and maintainable. The test code focuses on the login page’s high-level behavior, such as logging in and checking if the user is logged in. It abstracts away the details of interacting with the page, such as finding the username and password fields and clicking the submit button.

Patterns to avoid

Interacting with the page using low-level details.

The term for using low-level details in software development, specifically in the context of writing automated tests, is called “Implementation Detail Testing.”

Implementation detail testing refers to writing tests that focus on the internal implementation of the code, such as the specific elements and methods used to interact with the page, rather than the higher-level behavior of the code. This type of testing can lead to brittle, hard-to-maintain tests that break when the implementation changes.

On the other hand, tests that focus on high-level behavior, and abstract away the low-level details, are called “Behavior Driven Testing” or “Functional Testing”. This testing helps ensure that the code behaves as expected, independent of the specific implementation details.

Therefore, it’s important to use Page Objects to abstract away the low-level details and focus on high-level behavior to create maintainable and reusable tests.

import { Page, launch } from 'playwright';
import { LoginPage } from './LoginPage';
describe('Login Page', () => {
let browser: any;
let page: Page;
let loginPage: LoginPage;
beforeEach(async () => {
browser = await launch({ headless: true });
page = await browser.newPage();
loginPage = new LoginPage(page);
await loginPage.visit();
});
afterEach(async () => {
await browser.close();
});
test('user can login', async () => {
await page.type('#username', 'testuser');
await page.type('#password', 'password');
await page.click('#submit');
const isLoggedIn = await page.evaluate(() => {
return document.querySelector('#logout') !== null;
});
expect(isLoggedIn).toBe(true);
});
});

In this example, the test block defines a test for the login page, but instead of using the login() method of the LoginPageclass, it directly interacts with the page by finding the username and password fields and clicking the submit button. This makes the test code more difficult to understand because it's harder to know the intent of the test, and it also makes the test less reusable and maintainable because you have to change the test code if the structure of the page changes.

A better approach would be to focus on the high-level behavior of the login page, such as logging in and using the login() method of the LoginPage class to handle the low-level details of interacting with the page, like finding the input fields and clicking the submit button. This makes the test code more readable and maintainable, and it also makes the test more reusable because you can use the same method for logging in to other tests.

You can run this code in a CICD Pipeline in Azure as well.

Are you struggling to keep up with the maintenance and scalability of your automation tests? Want to improve the efficiency and effectiveness of your test automation efforts? Schedule your free consultation with us today and let our experts guide you toward a more robust and sustainable automation strategy for your organization.

Conclusion

In conclusion, mastering Page Objects in Playwright is essential for creating effective test automation. By following the principles of single responsibility, abstraction, and encapsulation and using Playwright’s powerful features, you can create Page Objects that are maintainable, reusable, and easy to understand. Whether you’re new to test automation or an experienced developer, this blog post has provided you with valuable insights and techniques for creating effective Page Objects with Playwright. By using these techniques, you can ensure that your tests are reliable, efficient, and easy to maintain, helping to ensure the success of your

--

--

Nikolay Advolodkin
Geek Culture

Nikolay Advolodkin is a self-driven SDET on a lifelong mission to create profound change in the IT world and ultimately leave a legacy for his loved ones