Using the Playwright Page Object Model

How to add code reusability to your frontend end-to-end testing

ยท

5 min read

Why do we need the Page Object Model?

The Page Object Model is another feature offered by Playwright to improve your code.

Suppose you want to use the same elements in several different tests - you already went to the trouble of locating these elements with locators - if you write a new test, you will have to re-locate all of those elements...unless there were a way to reuse the code you already wrote.

Turns out there is! Introducing the Page Object Model (or POM), which leverages the page object to help your code be more reusable.

What is a Page Object?

A page object represents a portion of your application. So for example, if I wanted to use a page object for my blog, I could write one to represent the main page of my blog, and another page object to represent a post page.

How does the POM work?

Essentially, we're going to break the test up into two parts: the page object part and the test part.

I'm going to use a variant of the code that I explain here, and show you how we can convert it to use a POM. The following test has 3 steps, each delineated by the test.step syntax. Overall we're going to verify that the content on the main page of my blog is visible.

const { test, expect } = require('@playwright/test');

test('verify main page content is visible and links', async ({ page }) => {
  await page.goto('https://madelinecaples.hashnode.dev/');

  await test.step('Expect a title "to contain" a substring', async () => {
    await expect(page).toHaveTitle('Madeline\'s Machine Learning Log');
  }); 

  await test.step('Expect header to contain image and correct title', async () => {
    const headerTitle = page.getByRole('link', { name: 'Madeline\'s Machine Learning Log'}); 
    await expect(headerTitle).toBeVisible();
    const headerTitleImage = headerTitle.getByRole('img'); 
    await expect(headerTitleImage).toBeVisible(); 
    await expect(headerTitleImage).toHaveAttribute('alt', 'Madeline Caples'); 
  }); 

  await test.step('check if blog author container has the expected elements', async () => {
    const blogAuthorContainer = page.locator('.blog-author-container'); 
    const blogAuthorImage = blogAuthorContainer.getByRole('img'); 
    await expect(blogAuthorImage).toBeVisible(); 
    await expect(blogAuthorImage).toHaveAttribute('alt', 'Madeline Caples'); 
    const blogAuthorTitle = blogAuthorContainer.getByRole('link').last();
    await expect(blogAuthorTitle).toHaveText('Madeline Caples'); 
    const headline = blogAuthorContainer.getByRole('paragraph'); 
    await expect(headline).toHaveText('I explain machine learning concepts in plain English');
  }); 
});

Notice places in the code where variables are set. Those are good candidates for being abstracted into a page object, since they are the elements we're going to use again and again. Once we make this abstraction, we can potentially use the same page object to write additional tests.

Page object syntax

To start, you're going to make a pages folder within your testing directory. The folder structure will look like this:

Screenshot of testing directory with node_moduls, pages, playwright-report, test-results, tests, and tests-examples folders, a gitignore file, package-lock.json, package.json, and playwright.config.js files

Within your new pages folder, add a file with the .pages.js extension. I'm going to name my file main.pages.js but you can feel free to change the "main" part to whatever suits your fancy.

In this file, we're going to start off by declaring a class BlogMainPage, that we'll export. Inside we'll add the page object provided by Playwright.

exports.BlogMainPage = class BlogMainPage {
    /**
     * @param {import('@playwright/test').Page} page
     */
}

We'll define a variable url, to point to the url we want to navigate to. We'll also add a constructor function inside the class. This is where we'll define the page and elements on the page that we want to use in our test.

exports.BlogMainPage = class BlogMainPage {

    /**
     * @param {import('@playwright/test').Page} page
     */

    url = 'https://madelinecaples.hashnode.dev'
    constructor(page) {
      this.page = page; 
    }
}

For our first element, let's convert headerTitle into a property inside the constructor.

this.headerTitle = page.getByRole('link', { name: 'Madeline\'s Machine Learning Log'});

Essentially, we're going to convert all of the variables from above into properties. So our constructor will look like this:

 constructor(page) {
      this.page = page; 
      this.headerTitle = page.getByRole('link', { name: 'Madeline\'s Machine Learning Log'});
      this.headerTitleImage = this.headerTitle.getByRole('img'); 
      this.blogAuthorContainer = page.locator('.blog-author-container'); 
      this.blogAuthorImage = this.blogAuthorContainer.getByRole('img'); 
      this.blogAuthorTitle = this.blogAuthorContainer.getByRole('link').last();
      this.headline = this.blogAuthorContainer.getByRole('paragraph').last(); 
    }

We can also define methods on our class, which can be useful if you have a function you'll be using more than once - like the goto() function that we need to use every time we want to go to a new page. As a method in your class (and below the constructor function) add goto().

async goto() {
        await this.page.goto(this.url);
}

Now inside of the test (which we'll write next), we can call BlogMainPage.goto() and we'll be able to navigate to the url we defined in the class for that page.

Test syntax

You'll still write your tests inside of your tests folder. The file will have the .test.js extension. I'm naming mine blog-main.test.js.

Test syntax is going to look very similar to previously, but we'll need to import our page object model that we wrote.

import { test, expect } from '@playwright/test';
import { BlogMainPage } from '../pages/main.page';

Then inside our test, we'll have to initiate our page object by calling the class. We'll save that to a variable called main.

test('Blog main page verification', async ({ page }) => {
    const main = new BlogMainPage(page);
});

To navigate to the appropriate page, we'll use the goto() method we created:

await main.goto();

Then we can start to use our properties to check out the various elements on our webpage and see if they are as we expect. We'll still use steps within our test to keep it organized. Remember that since we're using the page object called main now, we'll have to expect main.page to have a title, rather than simply page.

 await test.step('Expect a title "to contain" a substring', async () => {
        await expect(main.page).toHaveTitle('Madeline\'s Machine Learning Log');
      });

Then in our second step, instead of finding the elements and assigning them to variables, all we need is the logic for actually running the test.

await test.step('Expect header to contain image and correct title', async () => {
        await expect(main.headerTitle).toBeVisible(); 
        await expect(main.headerTitleImage).toBeVisible(); 
        await expect(main.headerTitleImage).toHaveAttribute('alt', 'Madeline Caples'); 
      });

The rest of the test will look like this:

      await test.step('check if blog author container has the expected elements', async () => { 
        await expect(main.blogAuthorImage).toBeVisible(); 
        await expect(main.blogAuthorImage).toHaveAttribute('alt', 'Madeline Caples'); 
        await expect(main.blogAuthorTitle).toHaveText('Madeline Caples'); 
        await expect(main.headline).toHaveText('I explain machine learning concepts in plain English');
      }); 

      await test.step('expect all links on page to be enabled', async () => {
        for (const link of await main.page.getByRole('link').all()) {
            await expect(link).toBeEnabled(); 
          }
      });

Thanks for learning about Playwright's Page Object Model with me! If you'd like to go further with Playwright, stay tuned for my next post on Playwright's Visual Studio Code extension. ๐Ÿ˜Š

Resources for going further

Docs: https://playwright.dev/docs/pom

Description of classes on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes