Playwright Page Object Model (POM) | Online Tutorial
\\\\n\\\\nThis chapter introduces the design concept and implementation of the Page Object Model (POM), helping you organize large test suites.
\\\\n\\\\n\\\\n\\\\n
What is POM
\\\\n\\\\nThe Page Object Model is a test design pattern that encapsulates page elements and operations into independent classes.
\\\\n\\\\nEach page object represents a page or component in a web application, containing locators and operation methods for that page.
\\\\n\\\\n\\\\n\\\\n
Advantages of POM
\\\\n\\\\n| Advantage | \\\\nDescription | \\\\n
|---|---|
| Simplify test writing | \\\\nTest code uses high-level APIs (like loginPage.login('user', 'pass')) instead of raw Locator operations | \\\\n
| Reduce maintenance costs | \\\\nChanges to page structure only need to modify the Page Object class, not every test | \\\\n
| Improve readability | \\\\nTest code reads more like business language, understandable even by non-technical staff | \\\\n
| Avoid duplication | \\\\nSame locators and operations only need to be defined once | \\\\n
\\\\n\\\\n
Creating a Page Object Class
\\\\n\\\\nBelow is an example using the TUTORIAL website to create a Login Page Object.
\\\\n\\\\nExample
\\\\n\\\\n// File path: pages/LoginPage.ts\\\\n\\\\nimport{ type Page, type Locator, expect } from '@playwright/test';\\\\n\\\\nexport class LoginPage {\\\\n\\\\n readonly page: Page;\\\\n\\\\n// Declare all locators as read-only properties\\\\n\\\\n readonly usernameInput: Locator;\\\\n\\\\n readonly passwordInput: Locator;\\\\n\\\\n readonly loginButton: Locator;\\\\n\\\\n readonly errorMessage: Locator;\\\\n\\\\nconstructor(page: Page){\\\\n\\\\nthis.page= page;\\\\n\\\\n// Initialize all locators in the constructor\\\\n\\\\nthis.usernameInput= page.getByLabel('Username');\\\\n\\\\nthis.passwordInput= page.getByLabel('Password');\\\\n\\\\nthis.loginButton= page.getByRole('button',{ name:'Login'});\\\\n\\\\nthis.errorMessage= page.getByTestId('login-error');\\\\n\\\\n}\\\\n\\\\n// Navigate to LoginPage\\\\n\\\\n async goto(){\\\\n\\\\n await this.page.goto('/login');\\\\n\\\\n}\\\\n\\\\n// Perform Login operation\\\\n\\\\n async login(username: string, password: string){\\\\n\\\\n await this.usernameInput.fill(username);\\\\n\\\\n await this.passwordInput.fill(password);\\\\n\\\\n await this.loginButton.click();\\\\n\\\\n}\\\\n\\\\n// Verify the error message for Login failure\\\\n\\\\n async expectLoginError(message: string){\\\\n\\\\n await expect(this.errorMessage).toBeVisible();\\\\n\\\\n await expect(this.errorMessage).toContainText(message);\\\\n\\\\n}\\\\n\\\\n}\\\\n\\\\n\\\\n\\\\n\\\\n
Using Page Object in Tests
\\\\n\\\\nExample
\\\\n\\\\n// File path: tests/login.spec.ts\\\\n\\\\nimport{ test, expect } from '@playwright/test';\\\\n\\\\nimport{ LoginPage } from '../pages/LoginPage';\\\\n\\\\ntest.describe('LoginFunction',()=>{\\\\n\\\\n test('Correct Username Password Login successful', async ({ page })=>{\\\\n\\\\nconst loginPage =new LoginPage(page);\\\\n\\\\n// Use the method of the Page Object\\\\n\\\\n await loginPage.goto();\\\\n\\\\n await loginPage.login('tutorial_user','correct_password');\\\\n\\\\n// LoginVerify redirection after success\\\\n\\\\n await expect(page).toHaveURL('/dashboard');\\\\n\\\\n});\\\\n\\\\ntest('Incorrect Password Login failed', async ({ page })=>{\\\\n\\\\nconst loginPage =new LoginPage(page);\\\\n\\\\nawait loginPage.goto();\\\\n\\\\n await loginPage.login('tutorial_user','wrong_password');\\\\n\\\\n// Use the assertion method of the Page Object\\\\n\\\\n await loginPage.expectLoginError('UsernameorPasswordError');\\\\n\\\\n});\\\\n\\\\n});\\\\n\\\\n\\\\n\\\\n\\\\n
Multi-layer Page Objects
\\\\n\\\\nIn real projects, you can create multiple Page Objects that can reference each other.
\\\\n\\\\nExample
\\\\n\\\\n// File path: pages/DashboardPage.ts\\\\n\\\\nimport{ type Page, type Locator, expect } from '@playwright/test';\\\\n\\\\nexport class DashboardPage {\\\\n\\\\n readonly page: Page;\\\\n\\\\n readonly welcomeText: Locator;\\\\n\\\\n readonly logoutButton: Locator;\\\\n\\\\nconstructor(page: Page){\\\\n\\\\nthis.page= page;\\\\n\\\\nthis.welcomeText= page.getByText('Welcome back');\\\\n\\\\nthis.logoutButton= page.getByRole('button',{ name:'Exit'});\\\\n\\\\n}\\\\n\\\\nasync expectWelcomeMessage(username: string){\\\\n\\\\n await expect(this.welcomeText).toContainText(username);\\\\n\\\\n}\\\\n\\\\nasync logout(){\\\\n\\\\n await this.logoutButton.click();\\\\n\\\\n}\\\\n\\\\n}\\\\n\\\\n\\\\nCombining Page Objects:
\\\\n\\\\nExample
\\\\n\\\\ntest('Complete LoginLogout flow', async ({ page })=>{\\\\n\\\\nconst loginPage =new LoginPage(page);\\\\n\\\\nconst dashboardPage =new DashboardPage(page);\\\\n\\\\n// Login\\\\n\\\\n await loginPage.goto();\\\\n\\\\n await loginPage.login('tutorial_user','password');\\\\n\\\\n// Verify dashboard\\\\n\\\\n await dashboardPage.expectWelcomeMessage('tutorial_user');\\\\n\\\\n// Logout\\\\n\\\\n await dashboardPage.logout();\\\\n\\\\n await expect(page).toHaveURL('/login');\\\\n\\\\n});\\\\n\\\\n\\\\n\\\\n\\\\n
POM Best Practices
\\\\n\\\\n| Practice | \\\\nDescription | \\\\n
|---|---|
| Locators in constructor | \\\\nDefine all locators in the constructor, don't create them dynamically in methods | \\\\n
| Methods return Page Object | \\\\nOperation methods can return new Page Objects (e.g., return DashboardPage after login) | \\\\n
| Encapsulate assertions but don't overuse | \\\\nEncapsulate commonly used assertion combinations as methods, write simple assertions directly in tests | \\\\n
| Use composition over inheritance | \\\\nAvoid deep inheritance, use composition pattern to associate multiple Page Objects | \\\\n
| One object per page/component | \\\\nAppropriate granularity, don't create separate classes for every small element | \\\\n
\\\\n\\\\n\\\\n \\\\nPOM is not mandatory.
\\\\nFor small projects with only a few tests, writing Locators directly in tests may be clearer. When you have more than 5 test files, consider introducing POM.
\\\\n
YouTip