Akar Masalah: Hydration Race Condition pada SSR Nuxt 3
Flaky test pada pengujian End-to-End (E2E) aplikasi Nuxt 3 umumnya bersumber dari hydration race condition. Pada arsitektur Server-Side Rendering (SSR), server mengirimkan markup HTML yang sudah ter-render secara utuh ke browser sebelum JavaScript bundle selesai diunduh, di-parse, dan dieksekusi oleh Vue runtime.
Playwright memiliki mekanisme actionability checks bawaan. Sebelum menjalankan aksi seperti click(), Playwright memvalidasi apakah elemen target telah terpasang di DOM (attached), terlihat (visible), stabil secara posisi, dan dapat menerima pointer events (enabled). Karena elemen HTML hasil SSR sudah berada di DOM, Playwright menganggap elemen tersebut siap menerima interaksi.
Masalah terjadi jika Playwright memicu aksi klik pada celah waktu antara rendering DOM statis dan tuntasnya proses hidrasi Vue. Pada interval mikro ini, event listener (misalnya @click atau v-on:click) belum terikat pada node DOM. Akibatnya, sinyal klik diterima oleh browser tetapi diabaikan oleh aplikasi, menyebabkan aksi gagal tanpa memunculkan error eksplisit pada Playwright.
Anti-Pattern: Mengapa waitForTimeout Harus Dihindari
Solusi instan yang sering digunakan pengembang adalah menyisipkan jeda statis:
// ANTI-PATTERN: Jangan gunakan pendekatan ini
await page.goto('/dashboard');
await page.waitForTimeout(2000);
await page.locator('button#submit').click();Pendekatan ini memiliki sejumlah kelemahan fatal:
- Non-deterministik: Waktu 2000ms mungkin cukup di mesin lokal, tetapi gagal di CI server yang memiliki alokasi CPU terbatas ketika beban komputasi meningkat.
- Memperlambat pipeline CI: Akumulasi sleep statis di puluhan test case menambah durasi build secara eksponensial tanpa memberikan jaminan reliabilitas.
- Menyembunyikan regresi performa: Jika hidrasi aplikasi melambat akibat bundle size membengkak, penambahan timeout hanya menutupi masalah arsitektur tersebut.
Solusi Deterministik: Menangkap Lifecycle Nuxt via Client Flag
Solusi yang reliabel adalah memastikan test runner hanya berinteraksi dengan elemen setelah hook hidrasi Nuxt selesai dieksekusi. Kita dapat memanfaatkan lifecycle hook app:mounted atau app:suspense:resolve melalui client-side plugin Nuxt untuk menginjeksikan state global ke objek window.
1. Buat Nuxt Client Plugin
Buat file plugin khusus client untuk menandai selesainya hidrasi:
// plugins/hydration-flag.client.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('app:suspense:resolve', () => {
window.__NUXT_HYDRATED__ = true;
});
});Tambahkan deklarasi tipe global TypeScript agar compiler mengenali properti tersebut jika diperlukan:
// types/global.d.ts
export {};
declare global {
interface Window {
__NUXT_HYDRATED__?: boolean;
}
}Implementasi Custom Playwright Fixture
Alih-alih memanggil page.waitForFunction() secara repetitif di setiap test file, buat custom fixture Playwright yang secara otomatis menunggu hidrasi tuntas setiap kali navigasi dilakukan.
2. Buat Fixture Extended
// tests/fixtures/test.ts
import { test as base, type Page } from '@playwright/test';
export const test = base.extend<{
hydratedPage: Page;
}>({
hydratedPage: async ({ page }, use) => {
// Intercept navigasi untuk menunggu hidrasi Nuxt
const originalGoto = page.goto.bind(page);
page.goto = async (url, options) => {
const response = await originalGoto(url, options);
// Tunggu flag window.__NUXT_HYDRATED__ bernilai true
await page.waitForFunction(() => window.__NUXT_HYDRATED__ === true, null, {
timeout: 10_000,
});
return response;
};
await use(page);
},
});
export { expect } from '@playwright/test';Contoh Test Spec Deterministik
Gunakan fixture hydratedPage pada test spec untuk memastikan event handler sudah aktif sebelum input atau interaksi tombol dieksekusi.
// tests/e2e/auth-modal.spec.ts
import { test, expect } from '../fixtures/test';
test.describe('Autentikasi User', () => {
test('harus membuka modal login saat tombol diklik', async ({ hydratedPage: page }) => {
await page.goto('/login');
const triggerButton = page.locator('button#open-modal');
const modalDialog = page.locator('[role="dialog"]');
// Interaksi dijamin aman dari hydration race
await triggerButton.click();
await expect(modalDialog).toBeVisible();
await expect(modalDialog).toContainText('Masuk ke Akun Anda');
});
});Konfigurasi Playwright dan CI untuk Mencegah False Negative
Selain penanganan hidrasi di level kode pengujian, konfigurasi test runner dan pipeline CI harus dioptimalkan untuk mengurangi flakiness.
1. Selalu Uji Terhadap Production Build di CI
Menjalankan test E2E di atas Nuxt development server (nuxi dev) meningkatkan flakiness secara drastis karena overhead Vite HMR dan transpilation on-the-fly. Selalu build aplikasi terlebih dahulu:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
retries: process.env.CI ? 2 : 0,
// Hindari starvation CPU di runner CI
workers: process.env.CI ? 2 : undefined,
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
webServer: {
command: 'node .output/server/index.mjs',
port: 3000,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});2. Konfigurasi GitHub Actions Workflow
Pastikan alur CI menjalankan tahapan build secara terpisah sebelum Playwright dieksekusi:
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install Dependencies
run: npm ci
- name: Build Nuxt Application
run: npm run build
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run Playwright Tests
run: npx playwright test
- name: Upload Artifacts on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: test-results/Dengan menyinkronkan interaksi Playwright terhadap lifecycle hidrasi aktual Nuxt 3 dan mengisolasi runner pada production build, celah eksekusi yang memicu event hilang dapat dieliminasi secara total.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!