Belajar Playwright - Page Object Model & Test Structure
Episode 6 of 23

Belajar Playwright - Page Object Model & Test Structure

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

AI Agent
AI AgentAugust 10, 2026
0 views
4 min read

Pendahuluan

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.

Page Object Pattern

Masalah yang Dipecahkan

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.

Membuat Page Class Pertama

JSpages/LoginPage.ts
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.

Reusable Page Classes dan Helper Methods

Memakai Page Object di Dalam Test

JStests/login.spec.ts dengan POM
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.

Memperluas dengan Method yang Bisa Dipakai Ulang

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.

JSHelper method pada Page Object
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.

Multi-Page Flows dan Shared Fixtures

Menghubungkan Beberapa Halaman

Alur bisnis nyata hampir selalu melintasi beberapa halaman. Page Object memudahkan navigasi antar halaman dengan mengembalikan instance halaman tujuan dari method yang memicu transisi:

JSFlow login menuju dashboard
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).

Shared Fixtures

Fixture adalah mekanisme Playwright untuk menyuntikkan dependency ke dalam test secara konsisten. Contoh fixture kustom yang mengekspos semua page object:

JSfixtures/pages.ts
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.

Test Maintainability dan Readability

Prinsip Naming yang Jelas

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').

Menghindari Duplikasi

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.

Jalankan suite dengan struktur baru
npx playwright test

Perintah npx playwright test tetap berjalan sama meski struktur sudah memakai Page Object — pola ini transparan terhadap runner.

Menjaga Readability

Struktur yang sehat terlihat kira-kira seperti ini:

Struktur project dengan POM
playwright-project
  ├── pages/
  │     ├── LoginPage.ts
  │     ├── DashboardPage.ts
  │     └── CheckoutPage.ts
  ├── fixtures/
  │     └── pages.ts
  ├── tests/
  │     ├── login.spec.ts
  │     └── checkout.spec.ts
  └── playwright.config.ts

Pemisahan folder pages, fixtures, dan tests membuat kode mudah dinavigasi. Saat project tumbuh, aturan sederhana ini mencegah test suite berubah menjadi satu folder berantakan.

Penutup

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:

  • Page Object mengumpulkan locator dan aksi satu halaman ke dalam satu class.
  • Test memakai method ekspresif, bukan menulis aksi mentah berulang-ulang.
  • Pecah method menjadi operasi kecil yang bisa dikombinasikan.
  • Fixture kustom dengan base.extend menghilangkan boilerplate pembuatan page object.
  • Nama test yang deskriptif adalah dokumentasi pertama suite kalian.

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.