e2e-testing-expert · git:20260816.d23736e · 2026-08-16 · sha256 bf6cc0798844e83e
e2e-testing-expert git:20260816.d23736eA
Immutable. This exact content is served forever at /api/v1/blob/bf6cc0798844e83e.
---
name: e2e-testing-expert
description: "Expert guide for End-to-End (E2E) testing with Playwright, unit/integration testing with Vitest, and CI/CD automated testing pipeline setup / Panduan ahli pengujian End-to-End (E2E) dengan Playwright, pengujian unit/integrasi dengan Vitest, dan otomatisasi CI/CD."
author: "vibes-plug-swarm"
---
# E2E Testing Expert (Playwright 1.49+ / Vitest 3 Edition)
[English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
---
<a name="english"></a>
## English
### Orchestration & Integration
Connects and orchestrates with relevant domain skills like `brainstorming`, `zero-to-prod-orchestrator`, and `project-context-mapper` to ensure cohesive execution.
### Description
Expert guide for building comprehensive automated test suites using **Playwright 1.49+** (E2E + component testing), **Vitest 3** (unit/integration), and robust CI/CD pipelines. Covers modern testing patterns for Next.js 15, React 19, and API backends.
### Trigger Conditions
- Writing E2E tests for web applications with Playwright.
- Writing unit or integration tests with Vitest.
- Setting up a full automated testing pipeline in GitHub Actions / GitLab CI.
- Testing React components in isolation with Playwright Component Testing.
- Mocking external services (APIs, databases) in tests.
- Setting up visual regression tests.
### Playwright 1.49+ — Key Updates
#### New in 2026
- **`aria-snapshot`**: Assert the accessibility tree as a snapshot — more semantic than DOM snapshots.
- **`page.addLocatorHandler()`**: Handle dynamic UI (modals, cookie banners) automatically.
- **WebSocket testing**: Built-in `page.expectWebSocketEvent()`.
- **Playwright MCP Server**: Expose a Playwright browser to AI agents via MCP tools.
#### Project Configuration
```typescript
// playwright.config.ts
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', { open: 'never' }],
['junit', { outputFile: 'results.xml' }],
],
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
// Setup: seed test DB
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
dependencies: ['setup'],
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
```
#### Page Object Model (POM)
```typescript
// tests/e2e/pages/LoginPage.ts
import { type Page, type Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
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();
await this.page.waitForURL('/dashboard');
}
}
```
#### API Mocking with `page.route()`
```typescript
test('shows error when API fails', async ({ page }) => {
// Intercept and mock API response
await page.route('**/api/users', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal server error' }),
contentType: 'application/json',
});
});
await page.goto('/users');
await expect(page.getByRole('alert')).toContainText('Something went wrong');
});
```
#### Accessibility Assertions (aria-snapshot)
```typescript
test('navigation is accessible', async ({ page }) => {
await page.goto('/');
// Assert accessibility tree structure
await expect(page.locator('nav')).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
- link "Pricing"
- link "Sign in"
`);
});
```
#### Auth State Reuse (storageState)
```typescript
// tests/e2e/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
// Save session state for reuse in all tests
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// tests/e2e/dashboard.spec.ts
import { test } from '@playwright/test';
test.use({ storageState: 'playwright/.auth/user.json' });
test('dashboard loads correctly', async ({ page }) => {
await page.goto('/dashboard');
// Already authenticated — no login needed
});
```
### Vitest 3 — Unit & Integration Testing
#### Configuration
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: { lines: 80, functions: 80 },
},
},
});
```
#### React Component Testing
```typescript
import { render, screen, userEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Counter } from './Counter';
describe('Counter', () => {
it('increments count on button click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
await user.click(screen.getByRole('button', { name: 'Increment' }));
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onMaxReached when limit hit', async () => {
const onMaxReached = vi.fn();
const user = userEvent.setup();
render(<Counter initialCount={9} max={10} onMaxReached={onMaxReached} />);
await user.click(screen.getByRole('button', { name: 'Increment' }));
expect(onMaxReached).toHaveBeenCalledOnce();
});
});
```
#### Server Action / API Route Testing
```typescript
import { describe, it, expect, beforeEach } from 'vitest';
import { testClient } from 'hono/testing';
import { app } from '../src/server';
import { resetTestDb } from './helpers/db';
describe('POST /api/users', () => {
beforeEach(resetTestDb);
it('creates a new user', async () => {
const client = testClient(app);
const res = await client.api.users.$post({
json: { name: 'Alice', email: 'alice@example.com' },
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.email).toBe('alice@example.com');
});
});
```
### GitHub Actions CI Pipeline
```yaml
# .github/workflows/test.yml
name: CI Tests
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm run test:unit --coverage
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
```
---
<a name="bahasa-indonesia"></a>
## Bahasa Indonesia
### Integrasi Orkestrasi
Terhubung dan mengorkestrasi skill domain yang relevan seperti `brainstorming`, `zero-to-prod-orchestrator`, dan `project-context-mapper` untuk memastikan eksekusi yang kohesif.
### Deskripsi
Panduan ahli untuk membangun test suite otomatis yang komprehensif menggunakan **Playwright 1.49+** (E2E + component testing), **Vitest 3** (unit/integrasi), dan pipeline CI/CD yang kuat. Mencakup pola pengujian modern untuk Next.js 15, React 19, dan backend API.
### Kondisi Pemicu
- Menulis E2E test untuk aplikasi web dengan Playwright.
- Menulis unit atau integration test dengan Vitest.
- Menyiapkan pipeline pengujian otomatis di GitHub Actions / GitLab CI.
- Menguji komponen React secara terisolasi.
- Mocking layanan eksternal (API, database) dalam test.
- Menyiapkan visual regression test.
### Playwright 1.49+ — Fitur Baru 2026
- **`aria-snapshot`**: Assert accessibility tree sebagai snapshot.
- **`page.addLocatorHandler()`**: Tangani UI dinamis (modal, cookie banner) secara otomatis.
- **WebSocket testing**: `page.expectWebSocketEvent()` bawaan.
- **Playwright MCP Server**: Ekspos browser Playwright ke agen AI via MCP.
### Page Object Model (POM)
Enkapsulasi selektor dan tindakan halaman dalam kelas terpisah untuk mengurangi duplikasi dan meningkatkan keterbacaan test.
### Mocking API dengan `page.route()`
Gunakan `page.route()` untuk mencegat permintaan jaringan dan mengembalikan respons palsu — mengisolasi frontend dari backend dalam E2E test.
### Aksesibilitas dengan aria-snapshot
Gunakan `toMatchAriaSnapshot()` untuk memvalidasi struktur aksesibilitas navigasi, form, dan komponen interaktif — lebih semantis dari snapshot DOM biasa.
### Auth State Reuse (storageState)
Gunakan `storageState` untuk menyimpan state sesi setelah login sekali dan menggunakannya kembali di semua test yang memerlukan autentikasi — menghemat waktu secara signifikan.
### Vitest 3 — Unit & Integration Testing
Vitest 3 adalah test runner yang cepat berbasis Vite dengan kompatibilitas Jest penuh. Gunakan untuk unit test React component, server action, dan logika bisnis.
### Pipeline CI GitHub Actions
Pisahkan job `unit` dan `e2e` untuk feedback paralel yang lebih cepat. Upload `playwright-report` sebagai artifact saat gagal untuk debugging.