1. What is Page Object Model (POM)?
Page Object Model (POM) is a design pattern used in test automation where:
- Each web page is represented as a class
- Locators and methods are stored separately from test files
- Tests become reusable, maintainable, and readable
2. Why Use POM?
Problems Without POM
Without POM:
- Duplicate locators everywhere
- Hard to maintain tests
- UI changes break multiple tests
- Large test files become messy
Benefits of POM
✅ Reusable code
✅ Easy maintenance
✅ Better readability
✅ Centralized locators
✅ Scalable framework
✅ Cleaner tests
3. Basic Folder Structure
project/
│
├── tests/
│ └── login.spec.ts
│
├── pages/
│ └── LoginPage.ts
│
├── utils/
│
├── fixtures/
│
├── playwright.config.ts
│
├── package.json
│
└── tsconfig.json
4. Install Playwright
Step 1: Create Project
mkdir playwright-pom
cd playwright-pom
Step 2: Initialize Node Project
npm init -y
Step 3: Install Playwright
npm init playwright@latest
Select:
- TypeScript
- Tests folder
- GitHub Actions (optional)
5. Understanding POM Structure
In POM:
| Component | Responsibility |
|---|---|
| Page Class | Stores locators + methods |
| Test File | Contains test scenarios |
| Fixtures | Shared setup |
| Utils | Reusable helper methods |
6. Create Your First Page Object
Example Website
We will automate:
- Login page
- Dashboard validation
7. Create Login Page Class
File
pages/LoginPage.ts
LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly username: Locator;
readonly password: Locator;
readonly loginBtn: Locator;
readonly dashboardText: Locator;
constructor(page: Page) {
this.page = page;
this.username = page.locator('#username');
this.password = page.locator('#password');
this.loginBtn = page.locator('#loginBtn');
this.dashboardText = page.locator('text=Dashboard');
}
async gotoLoginPage() {
await this.page.goto('https://example.com/login');
}
async enterUsername(user: string) {
await this.username.fill(user);
}
async enterPassword(pass: string) {
await this.password.fill(pass);
}
async clickLogin() {
await this.loginBtn.click();
}
async login(user: string, pass: string) {
await this.enterUsername(user);
await this.enterPassword(pass);
await this.clickLogin();
}
async verifyDashboardVisible() {
await expect(this.dashboardText).toBeVisible();
}
}
8. Create Test File
File
tests/login.spec.ts
login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('User Login Test', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.gotoLoginPage();
await loginPage.login('admin', 'admin123');
await loginPage.verifyDashboardVisible();
});
9. Execute Tests
npx playwright test
10. Run Specific Test
npx playwright test tests/login.spec.ts
11. View HTML Report
npx playwright show-report
12. Improve POM Design
Basic POM is good, but enterprise frameworks need more structure.
Better Folder Structure
project/
│
├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── BasePage.ts
│
├── tests/
│
├── fixtures/
│
├── test-data/
│
├── utils/
│
├── hooks/
│
├── api/
│
├── constants/
│
└── playwright.config.ts
13. Create Base Page
Why Base Page?
To avoid duplicate common methods:
- click
- fill
- wait
- navigate
- screenshot
BasePage.ts
import { Page, Locator } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(url: string) {
await this.page.goto(url);
}
async click(locator: Locator) {
await locator.click();
}
async fill(locator: Locator, value: string) {
await locator.fill(value);
}
async getTitle() {
return await this.page.title();
}
async wait(milliseconds: number) {
await this.page.waitForTimeout(milliseconds);
}
}
14. Extend Base Page
Updated LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
readonly username: Locator;
readonly password: Locator;
readonly loginBtn: Locator;
constructor(page: Page) {
super(page);
this.username = page.locator('#username');
this.password = page.locator('#password');
this.loginBtn = page.locator('#loginBtn');
}
async login(user: string, pass: string) {
await this.navigate('https://example.com/login');
await this.fill(this.username, user);
await this.fill(this.password, pass);
await this.click(this.loginBtn);
}
}
15. Create Dashboard Page
DashboardPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly dashboardHeader: Locator;
constructor(page: Page) {
this.page = page;
this.dashboardHeader = page.locator('h1');
}
async verifyDashboardLoaded() {
await expect(this.dashboardHeader).toHaveText('Dashboard');
}
}
16. Updated Test
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('Verify Login', async ({ page }) => {
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
await loginPage.login('admin', 'admin123');
await dashboardPage.verifyDashboardLoaded();
});
17. Best Practices for POM
1. Keep Locators Private to Page
❌ Bad
page.locator('#btn').click();
inside test files.
✅ Good
await loginPage.clickLogin();
2. Avoid Assertions Inside Page Methods
Page objects should perform actions.
Assertions should mostly remain in test files.
3. Use Meaningful Method Names
✅ Good
await loginPage.login();
❌ Bad
await loginPage.doAction1();
4. One Page = One Class
Each application page should have its own class.
5. Reuse Common Functions
Use:
- BasePage
- Utility classes
- Helper functions
18. Real Enterprise POM Structure
project/
│
├── pages/
│ ├── auth/
│ │ ├── LoginPage.ts
│ │ └── ForgotPasswordPage.ts
│ │
│ ├── dashboard/
│ │ └── DashboardPage.ts
│ │
│ └── common/
│ └── BasePage.ts
│
├── tests/
│
├── fixtures/
│
├── api/
│
├── utils/
│
├── constants/
│
├── test-data/
│
├── reports/
│
└── screenshots/
19. Advanced POM Concepts
Component Object Model
Reusable UI components:
- Navbar
- Sidebar
- Footer
- Modal
- Table
Example
export class HeaderComponent {
readonly profileIcon;
readonly logoutBtn;
constructor(page) {
this.profileIcon = page.locator('#profile');
this.logoutBtn = page.locator('#logout');
}
async logout() {
await this.profileIcon.click();
await this.logoutBtn.click();
}
}
20. POM with Fixtures
fixtures/baseTest.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
type MyFixtures = {
loginPage: LoginPage;
};
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
Usage
import { test } from '../fixtures/baseTest';
test('Login Test', async ({ loginPage }) => {
await loginPage.login('admin', 'admin123');
});
21. POM with Environment Variables
Install dotenv
npm install dotenv
.env
BASE_URL=https://example.com
USERNAME=admin
PASSWORD=admin123
playwright.config.ts
import dotenv from 'dotenv';
dotenv.config();
export default {
use: {
baseURL: process.env.BASE_URL
}
};
22. Common Mistakes in POM
❌ Huge Page Classes
Avoid:
- 2000-line page classes
Split by:
- Components
- Features
❌ Hardcoded Waits
Avoid:
waitForTimeout(5000)
Use:
- auto waiting
- expect
- waitForResponse
❌ Duplicate Locators
Centralize locators.
23. Industry-Level Recommendations
Recommended Stack
| Area | Tool |
|---|---|
| UI Automation | Playwright |
| Language | TypeScript |
| Framework | POM + Fixtures |
| Reporting | Allure |
| CI/CD | GitHub Actions |
| API Testing | Playwright API |
| Data | JSON / Faker |
| Parallel Run | Playwright Workers |
24. Final Enterprise Flow
Test File
↓
Page Object
↓
Base Page
↓
Playwright Engine
↓
Browser
25. Summary
POM Helps You
✅ Build scalable frameworks
✅ Reduce maintenance
✅ Improve readability
✅ Reuse code
✅ Create enterprise-level automation
26. Recommended Next Topics
After learning POM, learn:
- Playwright Fixtures
- API Testing
- Authentication Handling
- Parallel Execution
- Data-Driven Testing
- CI/CD Integration
- Retry Mechanism
- Playwright Hooks
- Allure Reporting
- Hybrid Framework Design