Episode ini membahas Page Object Pattern, reusable page classes, multi-page flows dan shared fixtures, serta maintainability suite.

Saat jumlah test mulai puluhan atau ratusan, menulis semua aksi langsung di dalam blok test() akan membuat suite sulit dirawat. Episode 6 ini membahas Page Object Model (POM) — pola desain paling populer untuk mengorganisasi test end-to-end — dan bagaimana memadukannya dengan fixtures bawaan Playwright.
Pola ini mengubah cara kalian berpikir: dari menulis test menjadi merancang antarmuka halaman. Setiap halaman aplikasi dipetakan menjadi sebuah class yang membungkus locator dan aksi. Test kemudian cukup memanggil method yang ekspresif, sehingga intent test terbaca jelas dan perubahan UI hanya perlu diedit di satu tempat.
Tanpa Page Object, satu perubahan kecil pada struktur halaman — misalnya mengganti atribut data-testid sebuah tombol — bisa memaksa kalian mengedit belasan test sekaligus. Locator juga tersebar di mana-mana, membuat suite sulit dipahami siapa melakukan apa.
Dengan Page Object, seluruh detail lokasi dan interaksi dikumpulkan di satu class per halaman. Test hanya memakai method publik dari class tersebut. Hasilnya: perubahan UI cukup diedit sekali, dan test menjadi dokumen hidup tentang perilaku aplikasi.
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(readonly page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Masuk' });
}
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();
}
}Perhatikan bahwa setiap locator dideklarasikan sekali sebagai property class. page.getByLabel('Email') dievaluasi ulang setiap kali dipakai, jadi aman untuk aplikasi dinamis. Method login(email, password) membungkus urutan aksi menjadi satu operasi yang bermakna.
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login sukses mengarah ke dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'rahasia123');
await expect(page).toHaveURL(/dashboard/);
});Dibanding menulis page.getByLabel('Email').fill(...) berulang-ulang, blok test di atas jauh lebih ringkas. new LoginPage(page) menerima fixture page sebagai satu-satunya dependency — ini pola yang konsisten dengan cara Playwright menyerahkan object ke test.
Class halaman sebaiknya hanya berisi operasi yang memang dibutuhkan banyak test. Hindari menulis satu method raksasa yang menirukan seluruh alur bisnis; pecah menjadi operasi kecil seperti isErrorMessageVisible() atau getTitle(). Method kecil lebih mudah dikombinasikan dan lebih mudah di-debug saat gagal.
export class CheckoutPage {
constructor(readonly page: Page) {}
async tambahKeKeranjang() {
await this.page.getByRole('button', { name: 'Tambah ke Keranjang' }).click();
}
async getTotalBelanja() {
return this.page.locator('#total-belanja').textContent();
}
}getTotalBelanja() mengembalikan data, sementara tambahKeKeranjang() melakukan aksi. Pemisahan aksi dan query ini membuat test lebih mudah diverifikasi.
Alur bisnis nyata hampir selalu melintasi beberapa halaman. Page Object memudahkan navigasi antar halaman dengan mengembalikan instance halaman tujuan dari method yang memicu transisi:
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test('alur lengkap login ke dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'rahasia123');
const dashboard = new DashboardPage(page);
await expect(dashboard.userName).toBeVisible();
});Menginisialisasi page object baru pada page yang sama adalah pendekatan paling sederhana dan mudah diikuti. Saat konfigurasi semakin kompleks, pindahkan pembuatan page object ke dalam fixture kustom agar test tidak perlu mengulang new LoginPage(page).
Fixture adalah mekanisme Playwright untuk menyuntikkan dependency ke dalam test secara konsisten. Contoh fixture kustom yang mengekspos semua page object:
import { test as base, type Page } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
export const test = base.extend({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';Dengan fixtures ini, test bisa langsung menerima loginPage dan dashboardPage tanpa boilerplate. base.extend({...}) menciptakan test runner baru yang merupakan super-set dari bawaan.
Nama test dan method adalah dokumentasi pertama. Gunakan pola given-when-then atau kalimat deskriptif yang menjelaskan perilaku, bukan mekanisme. test('pengguna bisa login dengan email dan password valid') jauh lebih informatif daripada test('login test 1').
Jika pola yang sama muncul di banyak test — misalnya menyiapkan data atau login terlebih dahulu — ekstrak ke fixture atau helper. Duplikasi adalah musuh utama maintainability; setiap duplikasi berarti perubahan di beberapa tempat sekaligus.
npx playwright testPerintah npx playwright test tetap berjalan sama meski struktur sudah memakai Page Object — pola ini transparan terhadap runner.
Struktur yang sehat terlihat kira-kira seperti ini:
playwright-project
├── pages/
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── CheckoutPage.ts
├── fixtures/
│ └── pages.ts
├── tests/
│ ├── login.spec.ts
│ └── checkout.spec.ts
└── playwright.config.tsPemisahan folder pages, fixtures, dan tests membuat kode mudah dinavigasi. Saat project tumbuh, aturan sederhana ini mencegah test suite berubah menjadi satu folder berantakan.
Episode 6 membekali kalian pola organisasi test yang akan dipakai sepanjang sisa series: Page Object Pattern membungkus locator dan aksi per halaman ke dalam class reusable, fixtures kustom menyuntikkan page object ke test tanpa boilerplate, dan prinsip naming yang jelas menjaga readability serta maintainability suite.
Inti yang harus dibawa pulang:
base.extend menghilangkan boilerplate pembuatan page object.Di episode 7 selanjutnya kita akan membahas assertions dan debugging — built-in assertions dan matchers, custom dan soft assertions, serta perangkat debugging seperti playwright codegen, mode debug, dan trace viewer yang akan menjadi senjata utama saat test gagal.