Introduction
Page object models is a best practice for Playwright to organize test suites by representing various components of a web application. For instance, in the context of a website, pages such as the login , home page, product listings page, etc. can each be encapsulated within a corresponding page objects.
Breaking down the Page Object: Understanding its Components
A Page Object serves as a container for all interactions and elements found on a particular web page or a segment of it. Within this structure, there are three fundamental components:
- Element Selectors: These serve as the blueprints pinpointing specific elements residing on the web page.
- Methods: These functions encapsulate various interactions with the web elements, simplifying complex operations into manageable actions.
- Properties: These encompass any supplementary information or attributes pertaining to the page, such as its unique URL or other metadata.
Step by Step Guide to write POM for the project
Identify Properties
Create pages folder. To create an abstraction for common methods and properties, we will first create base.page.ts to hold page property.
import { Page } from '@playwright/test'
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
}
Then create login.page.ts file which will contain abstraction for Login Page. Extend LoginPage class with BasePage class to inherit page property.
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../base.page';
export class LoginPage extends BasePage {
constructor(page: Page) {
super(page);
}
};
Identify Locators
Add locators for the elements on the Login page:
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../base.page';
export class LoginPage extends BasePage {
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
super(page);
this.usernameInput = page.locator('[data-test="username"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-button"]');
}
};
Identify Methods
Write methods which describe actions you might reuse in test cases:
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../base.page';
export class LoginPage extends BasePage {
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
super(page);
this.usernameInput = page.locator('[data-test="username"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-button"]');
}
async enterCredentials(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
}
async clickLoginButton() {
await this.loginButton.click();
}
}
Identify Assertions
import { Locator, Page, expect } from '@playwright/test'
import { BasePage } from '../base.page';
export class LoginPage extends BasePage {
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
super(page);
this.usernameInput = page.locator('[data-test="username"]');
this.passwordInput = page.locator('[data-test="password"]');
this.loginButton = page.locator('[data-test="login-button"]');
}
async enterCredentials(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
}
async clickLoginButton() {
await this.loginButton.click();
}
async IsSignedIn() {
await expect(this.page.getByText('Products')).toBeVisible();
}
}
Use Page Objects in test cases
With the abstraction provided by the Page Object, we can easily integrate it into our test cases. This involves initializing the object and invoking its functions whenver needed.
import { test } from '../utils/fixtures';
import expect from "@playwright/test"
import { LoginPage } from "../pages/login/login.page"
test.beforeEach(async ({page}) => {
await page.goto('https://www.saucedemo.com/');
});
test("login successfully", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.enterCredentials("standard_user", "secret_sauce");
await loginPage.clickLoginButton();
await loginPage.IsSignedIn();
});
}
Looks good now! However there is one more improvement we can make to avoid duplication of objects initialisation in each and every test case. For this purpose, Playwright provides fixtures which are reusable between test files. You can define pages once and use in all your tests.
Using Fixtures with Page Object Patterns
That’s how Playwright’s built-in page fixture could be implemented:
import { test as base } from "@playwright/test"
import { LoginPage } from "../pages/login/login.page"
export const test = base.extend({
loginPage: async ({page}, use) => {
// Set up the fixture
const loginPage = new LoginPage(page);
// Use the fixture value in the test
await use(loginPage);
}
})
In order to use fixture, you have to mention fixture in your test function argument, and test runner will take care of it.
import { test } from '../utils/fixtures';
import expect from "@playwright/test"
test.beforeEach(async ({page}) => {
await page.goto('https://www.saucedemo.com/');
});
test("login successfully", async ({ loginPage }) => {
await loginPage.enterCredentials("standard_user", "secret_sauce");
await loginPage.clickLoginButton();
await loginPage.IsSignedIn();
});
}
Fixture helped us to reduce number code lines and improve maintainability.
Bonus: Create Datafactory to store Users Data and Parametrize Test Case.
To centralize all the data utilized within our test cases, let’s establish a dedicated location. For this purpose, we will create /datafactory folder and login.data.ts file to store usernames and passwords needed to test an application. Also, important to remember establishing interfaces and types which will validate data we store.
export interface USERS {
username: string;
password: string;
}
type userTypes =
"standard_user" |
"locked_out_user" |
"problem_user" |
"performance_glitch_user"|
"error_user"|
"visual_user"
export const users: Record<userTypes, USERS> = {
"standard_user": {
username: "standard_user",
password: "secret_sauce",
},
"locked_out_user": {
username: "locked_out_user",
password: "secret_sauce",
},
"problem_user": {
username: "problem_user",
password: "secret_sauce",
},
"performance_glitch_user": {
username: "performance_glitch_user",
password: "secret_sauce",
},
"error_user": {
username: "error_user",
password: "secret_sauce"
},
"visual_user": {
username: "visual_user",
password: "secret_sauce"
}
}
And the last step: we have to parametrise test case we have with different target users. There are a lot of ways to do so, you can check in documentation for more information. For this demo, I am going to iterate through the object we have and test against each user.
import { test } from '../utils/fixtures';
import expect from "@playwright/test"
import { users } from '../utils/datafactory/login.data';
test.beforeEach(async ({page}) => {
await page.goto('https://www.saucedemo.com/');
});
for (const userType in users) {
test(`login successfully with ${userType}`, async ({ page, loginPage }) => {
await loginPage.enterCredentials(users[userType]["username"], users[userType]["password"]);
await loginPage.clickLoginButton();
await loginPage.IsSignedIn();
});
}
Execute Test Cases and Generate a Report.
Execute Test cases by running npx playwright test command from command line. As a result, report stores parametrised title for each test case by including name of the user.

Best Practices for Page Object Pattern
- Make Pages Small. Break down web pages into smaller, more manageable components to improve readability and maintainability of the page objects, ensuring each object focuses on a specific functionality or section of the page.
- Separate Actions and Assertions. Maintain a clear distinction between actions, such as interacting with elements, and assertions, which verify expected outcomes. This separation enhances the clarity and maintainability of test cases, facilitating easier troubleshooting and debugging.
- Keep a Minimum Number of Assertions in Test Cases. Limit the number of assertions within each test case to maintain clarity and focus. By reducing complexity, it becomes easier to pinpoint the cause of a failed test case, ensuring that the reason for failure is readily identifiable.
Conclusion
In this article, we explored the implementation of the Page Object Model (POM), a powerful design pattern that abstracts crucial elements like page properties, locators, actions, and assertions. When implementing POM in Playwright, it’s essential to keep in mind best practices, such as creating distinct classes for each page, defining methods for user interactions, and integrating these page objects into your tests. Additionally, we also took a look at how to approach data handling and test parametrization.
Repository with the code you can find here.
Leave a comment