Menerapkan Page Object Model untuk maintainability: page objects, abstraction layers, selectors terpusat, dan refactor test suite yang scalable

Setelah di episode 6 kita memahami Cypress vs Playwright, pada episode ini kita membahas pattern paling penting untuk maintainability: Page Object Model (POM). Tanpa POM, test suite menjadi sulit dipelihara saat aplikasi berkembang.
POM memisahkan locator/interaction dari test logic. Setiap halaman web di-representasikan sebagai class yang berisi selectors dan methods untuk berinteraksi dengan halaman tersebut.
// Test langsung pakai selectors — sulit dipelihara
test('login', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('#email-input', 'user@test.com');
await page.fill('#password-field', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
// Jika #email-input berubah, harus update di SEMUA tests// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByRole('textbox', { name: 'Email' });
this.passwordInput = page.getByRole('textbox', { name: 'Password' });
this.submitButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.locator('.error-message');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});project/
├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ ├── ProductsPage.ts
│ ├── CartPage.ts
│ └── CheckoutPage.ts
├── tests/
│ ├── login.spec.ts
│ ├── dashboard.spec.ts
│ └── checkout.spec.ts
└── playwright.config.ts// pages/BasePage.ts
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async waitForLoading() {
await this.page.locator('.loading').waitFor({ state: 'hidden' });
}
async getAlertMessage(): Promise<string> {
return this.page.locator('.alert').textContent() ?? '';
}
}
// pages/LoginPage.ts
export class LoginPage extends BasePage {
// Inherits BasePage methods
}// Return this untuk method chaining
async fillEmail(email: string) {
await this.emailInput.fill(email);
return this;
}
async fillPassword(password: string) {
await this.passwordInput.fill(password);
return this;
}
// Usage
await loginPage.fillEmail('user@test.com')
.fillPassword('password123')
.submit();Tip
POM bukan hanya tentang selectors — juga tentang business logic. Methods seperti login() atau addToCart() mengabstract complexity test.
// Refactor existing test ke POM
// 1. Buat page class
// 2. Pindahkan selectors ke page class
// 3. Buat methods untuk interactions
// 4. Update tests untuk use page class
// 5. Verify tests still passDi episode 8 selanjutnya kita akan membahas API testing automation — request/test frameworks, contract testing, dan mocking. Sampai jumpa di episode 8!