Pengujian end-to-end (E2E) pada aplikasi Next.js App Router sering kali mengalami flakiness ketika fitur Streaming Server-Side Rendering (SSR) dan React Suspense diaktifkan. Pengujian yang lulus di mesin lokal sering gagal di lingkungan Continuous Integration (CI) dengan galat timeout atau stale element reference. Masalah ini berakar pada ketidaksesuaian antara siklus hidup pertukaran elemen DOM selama streaming dan mekanisme penungguan pada Playwright.
Akar Masalah: Race Condition Streaming SSR dan Hydration
Saat menggunakan Next.js App Router dengan boundary <Suspense> atau konvensi loading.tsx, server mengirimkan respons awal berupa shell HTML beserta representasi visual sementara (skeleton fallback). Koneksi HTTP dipertahankan terbuka menggunakan Transfer-Encoding: chunked. Begitu asynchronous data fetch pada Server Component selesai di-resolve, server memancarkan chunk HTML tambahan bersama tag skrip inline yang memuat payload React Server Components (RSC).
Kondisi ini memicu tiga titik kegagalan pada pengujian otomatis:
- DOM Node Replacement: Saat chunk tiba, runtime React di klien mengeksekusi penggantian node DOM skeleton menjadi antarmuka final. Jika locator Playwright berinteraksi tepat pada interval pertukaran ini, instruksi pengujian akan mengenai node yang terlepas (detached DOM node).
- Hydration Lag: Tombol atau elemen interaktif mungkin sudah tampak secara visual via streaming HTML sebelum JavaScript bundle selesai diunduh dan dihidrasi. Event listener belum terpasang, menyebabkan aksi
click()gagal memicu efek samping. - Network Jitter Upstream: Durasi resolusi data server berbeda-beda. Penundaan jaringan beberapa milidetik dapat melampaui timeout statis pengetesan sebelum fallback digantikan oleh UI terminal.
Anti-Pattern: Kode Pengujian yang Rentan (Flaky)
Pola imperatif tradisional yang mengandalkan waitForSelector, CSS class, atau jeda waktu statis tidak kompatibel dengan streaming runtime Next.js.
// ❌ ANTI-PATTERN: Rentan terhadap flakiness
test('render metrik analytics secara benar', async ({ page }) => {
await page.goto('/analytics');
// Menunggu skeleton hilang secara manual
await page.waitForSelector('.skeleton-metric', { state: 'detached' });
// Menggunakan sleep statis untuk menunggu hidrasi selesai
await page.waitForTimeout(2000);
// Locator berbasis CSS class struktural yang rentan
const metricText = await page.locator('.metric-card > .value').textContent();
expect(metricText).toBe('$45,200');
});Kode di atas gagal karena waitForTimeout bersifat non-deterministik di server CI yang padat beban kerja. Selain itu, pengecekan penghapusan skeleton terpisah dari pengecekan kemunculan data final, membuka peluang kondisi balapan antara destruksi fallback dan konstruksi komponen riil.
Solusi 1: Web-First Assertions dan Accessible Role Locators
Playwright menyediakan web-first assertions yang memiliki mekanisme auto-retry bawaan. Assertion ini melakukan polling periodik terhadap DOM hingga kondisi terpenuhi atau batas timeout tercapai. Hindari menargetkan skeleton atau CSS internal; targetkan kondisi akhir (terminal state) langsung menggunakan User-Facing Locator.
// ✅ DETERMINISTIK: Mengandalkan Web-First Assertions & Role-based Locators
test('render metrik analytics secara benar', async ({ page }) => {
await page.goto('/analytics');
// Targetkan landmark struktural final
const metricSection = page.getByRole('region', { name: /ringkasan metrik/i });
// Web-first assertion secara otomatis menunggu:
// 1. Streaming chunk tiba di browser
// 2. React mengganti fallback skeleton
// 3. Elemen mencapai status visible dan teks terhidrasi
await expect(metricSection.getByRole('heading', { name: /total revenue/i })).toBeVisible();
await expect(metricSection.getByText('$45,200')).toBeVisible({ timeout: 10_000 });
});Mengapa pendekatan ini bekerja? Playwright mengevaluasi ulang locator di setiap siklus polling. Jika React mencopot DOM node di tengah pergantian skeleton ke UI final, locator akan mengevaluasi ulang pohon dokumen pada siklus polling berikutnya tanpa memicu kegagalan fatal.
Solusi 2: Mengeliminasi Jitter dengan Network Mocking
Flakiness pada Streaming Suspense kerap disebabkan oleh latensi tidak menentu dari backend microservice atau database dependensi. Isolasi layer presentasi Next.js dengan mencegat panggilan data eksternal menggunakan page.route.
test('render daftar transaksi tanpa terpengaruh latensi backend', async ({ page }) => {
// Mock endpoint API eksternal sebelum navigasi
await page.route('https://api.internal.company/v1/transactions', async (route) => {
// Berikan respons deterministik instan
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 'tx-101', amount: 'Rp 250.000', status: 'SUCCESS' },
{ id: 'tx-102', amount: 'Rp 100.000', status: 'PENDING' },
]),
});
});
await page.goto('/finance');
const list = page.getByRole('list', { name: /riwayat transaksi/i });
await expect(list.getByRole('listitem').first()).toContainText('Rp 250.000');
});Catatan Teknis: Jika Server Components melakukan fetch di level Node.js runtime server Next.js (bukan di browser client), page.route Playwright tidak akan mencegat request tersebut secara otomatis tanpa konfigurasi proxy. Gunakan tools seperti MSW (Mock Service Worker) pada level Node server jika fetch berlangsung strictly di server runtime.Solusi 3: Konfigurasi Concurrency dan Resource Throttling di CI
React streaming rendering membutuhkan siklus CPU yang konsisten untuk melakukan de-serialisasi RSC payload di sisi browser. Jika mesin worker CI kehabisan resource akibat konkurensi Playwright yang terlalu agresif, hidrasi JavaScript dapat tertunda secara signifikan, memicu kegagalan timeout aksi.
Sesuaikan playwright.config.ts untuk membatasi alokasi worker dan memperpanjang batas toleransi timeout di lingkungan otomatisasi:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
// Nonaktifkan eksekusi paralel penuh di CI untuk menghindari CPU starvation
workers: process.env.CI ? 2 : undefined,
// Berikan satu percobaan ulang terkontrol untuk menangani outlier transient di CI
retries: process.env.CI ? 1 : 0,
use: {
// Tingkatkan actionTimeout untuk menoleransi lag hidrasi saat CPU terbebani
actionTimeout: 10_000,
navigationTimeout: 15_000,
trace: 'on-first-retry',
},
expect: {
// Berikan window auto-retry yang cukup untuk chunk streaming
timeout: 7_000,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});Panduan Debugging Kasus Intermiten
Jika pengujian masih menunjukkan sifat intermiten setelah menerapkan strategi di atas, lakukan langkah investigasi berikut:
- Gunakan Playwright Trace Viewer: Periksa tab Action dan Console. Cari status DOM snapshot persis saat aksi klik dilakukan untuk melihat apakah elemen berada dalam kondisi disabled atau baru saja berganti dari fallback.
- Validasi Keberadaan Hydration Error: Buka console browser selama test run. Next.js akan melempar peringatan jika markup dari server berbeda dari markup yang dihidrasi di klien, yang sering menyebabkan React mereset seluruh sub-tree DOM.
- Pastikan Key Suspense Unik: Pastikan boundary
<Suspense key={...}>pada komponen Next.js memiliki key yang stabil guna mencegah re-mount DOM yang tidak perlu saat re-fetching data.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!