Using Page Objects As Custom Fixtures In Playwright

3 min readMay 19, 2024

The usage of Page Object Model (POM) is a widely adopted design pattern in test automation. It allows for the organizing of the respective page’s UI element locators and its related functions in a single class, enhancing the maintainability and readability of test scripts.

Playwright also supports the usage of Page Object Model. Let’s see how we can utilize it.

This is the page object of the Login page:

import {Locator, Page } from '@playwright/test';

export class LoginPage {
page: Page;
usernameInput: Locator;
passwordInput: Locator;
submitButton: Locator;

constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByTestId('username'),
this.passwordInput = page.getByTestId('password'),
this.submitButton = page.getByTestId('submit')
}

async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}

This is the page object of the item search page:

import { expect, type Locator, type Page } from '@playwright/test';

export class HomePage {
page: Page;
itemInput: Locator;
categoryInput: Locator;
searchButton: Locator;

constructor(page: Page) {
this.page = page;
this.itemInput = page.getByTestId('item'),
this.categoryInput = page.getByTestId('category'),
this.searchButton = page.getByTestId('search')
}

async searchItem(item: string, category: string) {
await this.itemInput.fill(username);
await this.categoryInput.fill(password);
await this.searchButton.click();
}

......
......
}

These are some simple tests:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { HomePage } from '../pages/home-page';
import { ProductDescription} from '../pages/product-description';

test('verify item search', async ({ page }) => {
const loginPage = new LoginPage(page);
const homePage = new HomePage(page);
await loginPage.login();
await homePage.searchItem('iPhone', 'mobile');
});

test('should add item to the cart', async ({ page }) => {
const loginPage = new LoginPage(page);
const homePage = new HomePage(page);
const productDescription = new ProductDescription(page);
await loginPage.login();
await homePage.searchItem('iPhone', 'mobile');
await productDescription.addToCartButton.click();
await expect(page).toHaveURL('cart');
});

Challenge

As seen above, the object instances of LoginPage and HomePage are created in every test function. If let’s say there are hundreds of tests, then it will be cumbersome to create the page object instances every time.

Solution

This can be avoided by using Playwright’s custom fixture. We can define and initialize required page objects as fixtures in one place and then use those fixtures in multiple test functions just like we use the default fixtures like page, browser, context, etc.

Creating a page object fixture

To create your own page object fixture, use test.extend() to create a new testobject that will include it.

// my-test.ts

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { HomePage } from '../pages/home-page';
import { ProductDescription} from '../pages/product-description';

type MyFixtures = {
loginPage: LoginPage;
homePage: HomePage;
productDescription: ProductDescriptionPage;
}

// Extend base test by providing custom page object fixture.
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
productDescription: async ({ page }, use) => {
await use(new ProductDescriptionPage(page));
}
});

export { expect } from '@playwright/test';

We have created two fixtures loginPage and homePage by defining page instance as an argument to their constructor.

Using page object as a fixture in the test

// test.spec.ts

import { expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
import { HomePage } from '../pages/home-page';
import { ProductDescription} from '../pages/product-description';
// importing new 'test' from 'my-test.ts' file
import { test } from './my-test';

test('verify item search', async ({ page, loginPage, homePage }) => {
await loginPage.login();
await homePage.searchItem('iPhone', 'mobile');
});

test('should add item to the cart', async ({ page, loginPage, homePage }) => {
const productDescription = new ProductDescription(page);
await loginPage.login();
await homePage.searchItem('iPhone', 'mobile');
await productDescription.addToCartButton.click();
await expect(page).toHaveURL('cart');
});

In the above, we have imported test from my-test.ts file. We can now define the page object fixtures in test() and directly use it to access the respective page object class properties without instantiating its class in every test().

This has enabled us to follow DRY (Do Not Repeat) principle as well.

Here’s the link to read more about Playwright’s custom fixtures.

Shiv Jirwankar
SDET

LinkedIn

--

--

Shiv Jirwankar
Shiv Jirwankar

Written by Shiv Jirwankar

Software Development Engineer in Test | An ambivert, optimistic, and karma believer | https://www.linkedin.com/in/shiv-jirwankar-45246577

No responses yet