Answer
Page Object Model in Playwright with TypeScript
The Page Object Model pattern separates element locators and actions from test logic, making tests readable and maintainable.
Base Page Class (Optional but Recommended)
TypeScript
// pages/BasePage.ts
import { Page } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async navigate(path: string): Promise<void> {
await this.page.goto(path);
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('domcontentloaded');
}
}
Login Page Object
TypeScript
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators as properties
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly signInButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.signInButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByTestId('error-message');
this.forgotPasswordLink = page.getByRole('link', { name: 'Forgot Password?' });
}
async goto(): Promise<void> {
await this.page.goto('/login');
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.signInButton.click();
}
async expectError(message: string): Promise<void> {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
Dashboard Page Object
TypeScript
// pages/DashboardPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly welcomeHeading: Locator;
readonly userAvatar: Locator;
readonly logoutButton: Locator;
readonly navItems: Locator;
constructor(page: Page) {
super(page);
this.welcomeHeading = page.getByRole('heading', { name: /Welcome/ });
this.userAvatar = page.getByTestId('user-avatar');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
this.navItems = page.locator('nav a');
}
async goto(): Promise<void> {
await this.page.goto('/dashboard');
}
async logout(): Promise<void> {
await this.logoutButton.click();
await expect(this.page).toHaveURL('/login');
}
async expectUserLoggedIn(name: string): Promise<void> {
await expect(this.welcomeHeading).toContainText(name);
await expect(this.userAvatar).toBeVisible();
}
}
Test Using Page Objects
TypeScript
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test.describe('Login Flow', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.goto();
});
test('valid login navigates to dashboard', async () => {
await loginPage.login('user@test.com', 'pass123');
await expect(loginPage.page).toHaveURL('/dashboard');
await dashboardPage.expectUserLoggedIn('Jane');
});
test('invalid credentials shows error', async () => {
await loginPage.login('wrong@test.com', 'wrongpass');
await loginPage.expectError('Invalid credentials');
});
test('user can logout', async () => {
await loginPage.login('user@test.com', 'pass123');
await dashboardPage.logout();
await expect(loginPage.page).toHaveURL('/login');
});
});
Project Structure
CODE
playwright-project/
āāā pages/
ā āāā BasePage.ts
ā āāā LoginPage.ts
ā āāā DashboardPage.ts
ā āāā ProductPage.ts
āāā tests/
ā āāā login.spec.ts
ā āāā products.spec.ts
āāā fixtures.ts
āāā playwright.config.ts
