Unit test memverifikasi komponen secara terpisah. E2E test memverifikasi bahwa aplikasi aktual Anda bekerja seperti yang dialami pengguna — dengan browser nyata, navigasi nyata, dan permintaan jaringan nyata. Playwright telah menjadi alat E2E testing dominan untuk Next.js di 2025: cepat (hingga 2x lebih cepat dari pesaing dalam benchmark), auto-wait untuk elemen tanpa panggilan sleep manual, dan menangani server components, streaming, dan hidrasi Next.js dengan benar.
Playwright menjalankan tes terhadap browser Chromium, Firefox, atau WebKit nyata — bukan lingkungan yang disimulasikan. Untuk aplikasi Next.js dengan server-side rendering, ini penting: Playwright melihat persis apa yang dilihat pengguna, termasuk HTML yang dirender server awal sebelum hidrasi. Ini menangani konten dinamis, status loading, dan navigasi dengan benar melalui auto-waiting bawaan.
Keputusan setup Playwright yang paling penting: tes terhadap build produksi, bukan server pengembangan. Server pengembangan mencakup middleware hot reloading, source maps, dan overlay error hanya-pengembangan yang tidak ada di produksi. Tes yang lulus dalam pengembangan dapat gagal di produksi jika optimisasi hanya-build atau jalur kode berbeda dipicu.
Page Object Model (POM) membungkus API tingkat rendah Playwright dalam antarmuka spesifik domain. Alih-alih `page.getByLabel('Email').fill('user@example.com')` tersebar di setiap tes, Anda menulis `loginPage.fillEmail('user@example.com')`. Ketika label input email berubah, Anda memperbarui satu file (kelas LoginPage) alih-alih setiap tes yang menggunakannya.
Playwright E2E Test Architecture for Next.js:
────────────────────────────────────────────────────────────
playwright.config.ts
├── webServer: { command: 'next build && next start', port: 3000 }
│ ↑ test against PRODUCTION build, not dev server
├── workers: 4 (parallel contexts, each isolated)
├── reporter: ['html', 'github']
└── projects:
├── setup: authenticate once, save storageState
├── chromium: { use: { storageState: 'auth.json' } }
└── firefox: { use: { storageState: 'auth.json' } }
Test execution flow:
setup project runs login flow → saves auth.json
│
▼
chromium / firefox projects load auth.json (skip login)
│
├── worker 1: invoice.spec.ts
├── worker 2: customer.spec.ts
├── worker 3: dashboard.spec.ts
└── worker 4: auth.spec.ts
│
▼
All tests run in parallel (~40% faster than serial)
Failed tests: screenshot + trace recorded for debugging
Selector priority (most to least resilient):
1. getByRole() — semantic, survives CSS changes ✓
2. getByLabel() — form accessibility label ✓
3. getByText() — visible text content ✓
4. getByTestId() — data-testid attribute ✓
5. CSS selectors — breaks on style refactoring ✗
6. XPath — brittle, avoid ✗Dari pengalaman saya menjalankan Playwright pada aplikasi ERP Next.js: siapkan persistensi status auth menggunakan fitur storageState Playwright. Masuk sekali dalam proyek setup, simpan sesi ke file, dan muat sesi itu di setiap tes yang memerlukan autentikasi. Tanpa ini, setiap tes yang memerlukan login melakukan alur login penuh — menambahkan 2-3 detik per tes. Dengan status auth yang disimpan, tes yang diautentikasi dimulai dengan sesi yang sudah aktif. Dalam suite 100 tes, ini menghemat 3-5 menit waktu CI.
Tes yang tidak stabil adalah alasan utama tim meninggalkan E2E testing. Penyebab: hard-coded waits (sleep(1000)), selector berdasarkan CSS class yang berubah selama refactoring, tes yang bergantung pada urutan eksekusi, dan tes yang berbagi status. Best practice Playwright: sukai selector semantik (getByRole, getByLabel, getByText) dibandingkan selector CSS atau XPath yang rusak pada perubahan gaya; jangan pernah gunakan `page.waitForTimeout()`.
Next.js 13+ App Router dengan server components dan Suspense streaming berarti konten halaman dimuat secara progresif. Playwright menangani ini dengan benar karena menggunakan perilaku browser nyata. Kuncinya: gunakan `expect(page.getByText('Daftar Faktur')).toBeVisible()` daripada memeriksa DOM segera setelah navigasi. Auto-waiter Playwright mencoba ulang assertion hingga 5 detik secara default, yang mencakup sebagian besar skenario streaming.
// playwright.config.ts — production-ready config
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html'], ['github']],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // record traces on failures
screenshot: 'only-on-failure',
},
projects: [
{ name: 'setup', testMatch: '**/auth.setup.ts' },
{ name: 'chromium', use: { ...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'] },
],
webServer: {
command: 'npm run build && npm start',
port: 3000,
reuseExistingServer: !process.env.CI,
},
})
// auth.setup.ts — login once, save state
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('admin@example.com')
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD!)
await page.getByRole('button', { name: 'Sign in' }).click()
await page.waitForURL('/dashboard')
await page.context().storageState({ path: 'playwright/.auth/user.json' })
})
// invoice.spec.ts — Page Object Model pattern
class InvoicePage {
constructor(private readonly page: Page) {}
async goto() { await this.page.goto('/invoices') }
async createInvoice(amount: number) {
await this.page.getByRole('button', { name: 'New Invoice' }).click()
await this.page.getByLabel('Amount').fill(String(amount))
await this.page.getByRole('button', { name: 'Save' }).click()
}
async expectSuccess() {
await expect(this.page.getByText('Invoice created')).toBeVisible()
}
}
test('creates an invoice', async ({ page }) => {
const invoicePage = new InvoicePage(page)
await invoicePage.goto()
await invoicePage.createInvoice(5000000)
await invoicePage.expectSuccess()
})Playwright berjalan headlessly di CI secara default. Alur kerja GitHub Actions: checkout kode, instal dependensi (termasuk browser Playwright dengan `npx playwright install --with-deps`), build aplikasi Next.js, dan jalankan `npx playwright test`. Playwright melaporkan kegagalan sebagai anotasi GitHub Actions — tes yang gagal muncul inline dalam diff PR.
Anti-pattern Playwright yang umum: menguji bahwa tombol memiliki CSS class tertentu, bahwa variabel state React memiliki nilai tertentu, atau bahwa node DOM tertentu ada. Tes-tes ini rusak kapan pun Anda merefactor implementasi, bahkan ketika pengalaman pengguna tidak berubah. Tulis tes dari perspektif pengguna: 'Ketika saya klik Submit, saya melihat pesan sukses' — bukan 'ketika submit handler formulir dipanggil, variabel successState bernilai true'.
Playwright menjalankan tes secara paralel secara default — setiap file tes mendapatkan konteks browser sendiri, dan beberapa konteks berjalan secara bersamaan di seluruh proses worker. Konfigurasikan `workers` di playwright.config.ts untuk mengontrol paralelisme. Untuk mesin CI dengan 4 core, `workers: 4` menjalankan 4 konteks browser secara paralel.
Playwright juga mendukung component testing — merender komponen React individual dalam browser nyata, secara terpisah. Ini berguna untuk komponen UI yang kompleks untuk diuji dengan jsdom: drag-and-drop, rendering canvas, animasi kompleks, atau komponen yang bergantung pada API browser. Untuk sebagian besar komponen, React Testing Library lebih cepat dan lebih sederhana.