1. Pengenalan Playwright
Playwright adalah framework testing end-to-end open-source yang dikembangkan oleh Microsoft. Playwright memungkinkan automasi browser untuk Chromium (Chrome, Edge), Firefox, dan WebKit (Safari) dengan satu API yang konsisten. Dirilis pada tahun 2020, Playwright dengan cepat menjadi salah satu tool testing paling populer.
Playwright dibangun oleh tim yang sebelumnya mengembangkan Puppeteer di Google, sehingga membawa pengalaman mendalam tentang browser automation. Keunggulan utama Playwright adalah dukungan cross-browser yang sejati, auto-waiting yang cerdas, dan fitur Trace Viewer yang revolusioner untuk debugging.
Keunggulan Playwright
| Keunggulan | Penjelasan |
|---|---|
| Cross-browser | Chromium, Firefox, WebKit — test di semua browser utama |
| Auto-waiting | Menunggu elemen siap secara otomatis sebelum berinteraksi |
| Trace Viewer | Visual debugging tool — lihat video, screenshot, dan DOM di setiap langkah |
| Codegen | Generate test code otomatis dari rekaman interaksi browser |
| Parallel Execution | Menjalankan test secara paralel bawaan tanpa tool tambahan |
| Multi-language | JavaScript, TypeScript, Python, Java, C# (.NET) |
| API Testing | Built-in API request context untuk testing backend |
| Network Interception | Mock dan modify network requests/responses |
| Mobile Emulation | Emulate perangkat mobile, geolokasi, timezone, locale |
Playwright vs Cypress vs Selenium
| Aspek | Playwright | Cypress | Selenium |
|---|---|---|---|
| Browser Support | 🟢 Chromium, FF, WebKit | 🟡 Chrome, FF, Edge | 🟢 Semua |
| Parallel Built-in | 🟢 Ya | 🔴 Perlu Dashboard | 🟡 Manual |
| Trace/Debugging | 🟢 Trace Viewer | 🟢 Time Travel | 🔴 Dasar |
| iFrame Support | 🟢 Penuh | 🔴 Terbatas | 🟢 Penuh |
| Multiple Tabs | 🟢 Ya | 🔴 Tidak | 🟢 Ya |
| API Testing | 🟢 Built-in | 🟡 cy.request | 🔴 Perlu library |
| Codegen | 🟢 Built-in | 🟡 Experimental | 🔴 Tidak |
| Speed | 🟢 Sangat cepat | 🟢 Cepat | 🟡 Sedang |
┌──────────────────────────────────────────────────────────┐
│ TEST CODE (Your Tests) │
│ │
│ test('login berhasil', async ({ page }) => { │
│ await page.goto('/login'); │
│ await page.getByLabel('Email').fill('budi@test.com'); │
│ await page.getByRole('button').click(); │
│ }); │
└──────────────────────────┬───────────────────────────────┘
│
┌────────────▼────────────┐
│ Playwright Library │
│ (Auto-wait, Retry) │
└────────────┬────────────┘
│
┌─────────────────┼─────────────────┐
│ │ │
┌─────▼──────┐ ┌──────▼───────┐ ┌──────▼──────┐
│ Chromium │ │ Firefox │ │ WebKit │
│ (Chrome, │ │ (Gecko) │ │ (Safari) │
│ Edge) │ │ │ │ │
└────────────┘ └──────────────┘ └─────────────┘
│ │ │
└─────────────────┼──────────────────┘
│
┌────────────▼────────────┐
│ Trace Viewer │
│ (Video, Screenshots, │
│ DOM Snapshots, Logs) │
└─────────────────────────┘
2. Instalasi & Setup
Instalasi
# Instal Playwright (dengan interactive setup) npm init playwright@latest # Atau manual install npm install -D @playwright/test # Install browser binaries (Chromium, Firefox, WebKit) npx playwright install # Install browser tertentu saja npx playwright install chromium npx playwright install chromium firefox # Install dengan system dependencies (untuk CI Linux) npx playwright install --with-deps # Cek instalasi npx playwright --version
Konfigurasi Playwright
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Direktori test files
testDir: './tests',
testMatch: '**/*.spec.{js,ts}',
// Global timeout per test
timeout: 30 * 1000, // 30 detik
// Assertion timeout
expect: {
timeout: 5000, // 5 detik untuk expect assertions
},
// Konfigurasi untuk CI/CD
fullyParallel: true, // Jalankan test secara paralel
forbidOnly: !!process.env.CI, // Gagal jika ada test.only di CI
retries: process.env.CI ? 2 : 0, // Retry 2x di CI
workers: process.env.CI ? 1 : undefined, // Workers di CI
// Reporter
reporter: [
['html', { open: 'never' }], // HTML report
['list'], // Console output
// ['json', { outputFile: 'test-results.json' }],
// ['junit', { outputFile: 'test-results.xml' }],
],
// Shared settings untuk semua projects
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // Rekam trace saat retry
screenshot: 'only-on-failure', // Screenshot saat gagal
video: 'retain-on-failure', // Video saat gagal
// Viewport
viewport: { width: 1280, height: 720 },
// Action timeout
actionTimeout: 10000,
navigationTimeout: 30000,
// Locale & timezone
locale: 'id-ID',
timezoneId: 'Asia/Jakarta',
// Ignore HTTPS errors
ignoreHTTPSErrors: true,
// HTTP headers
extraHTTPHeaders: {
'Accept-Language': 'id-ID,id;q=0.9',
},
},
// Konfigurasi per browser (Projects)
projects: [
// Setup project — jalankan sebelum semua test
{
name: 'setup',
testMatch: /.*\.setup\.js/,
},
// Desktop browsers
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Storage state dari auth setup
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
// Mobile viewports
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
// Tablet
{
name: 'tablet',
use: { ...devices['iPad Pro 11'] },
},
],
// Web server — auto-start dev server sebelum test
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
// Output folder
outputDir: 'test-results/',
});
Struktur Folder
project-root/ ├── tests/ │ ├── auth.setup.js ← Authentication setup │ ├── auth/ │ │ ├── login.spec.js │ │ └── register.spec.js │ ├── dashboard/ │ │ └── dashboard.spec.js │ └── homepage.spec.js ├── playwright/ │ └── .auth/ │ └── user.json ← Stored auth state (auto-generated) ├── test-results/ ← Test artifacts │ ├── screenshots/ │ └── videos/ ├── playwright-report/ ← HTML report │ └── index.html ├── playwright.config.js ← Konfigurasi └── package.json
3. Locators: Menemukan Elemen
Locators adalah cara Playwright menemukan elemen di halaman. Playwright merekomendasikan user-facing locators — selector yang merepresentasikan bagaimana pengguna melihat halaman, bukan struktur HTML internal.
Recommended Locators
import { test, expect } from '@playwright/test';
test.describe('Playwright Locators', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/demo');
});
test('getByRole — berdasarkan ARIA role', async ({ page }) => {
// Temukan button berdasarkan role dan name
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Beranda' }).click();
await page.getByRole('heading', { name: 'Dashboard' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@mail.com');
await page.getByRole('checkbox', { name: 'Setuju' }).check();
await page.getByRole('radio', { name: 'Gold' }).check();
await page.getByRole('combobox', { name: 'Kategori' }).selectOption('tech');
await page.getByRole('tab', { name: 'Settings' }).click();
await page.getByRole('menuitem', { name: 'Logout' }).click();
// Level heading
await page.getByRole('heading', { level: 1 }).waitFor();
await page.getByRole('heading', { level: 2, name: 'Profil' }).click();
});
test('getByLabel — berdasarkan label form', async ({ page }) => {
// Temukan input berdasarkan label
await page.getByLabel('Email').fill('budi@test.com');
await page.getByLabel('Password').fill('rahasia123');
await page.getByLabel('Ingat saya').check();
// Regex pattern
await page.getByLabel(/nama/i).fill('Budi Santoso');
});
test('getByPlaceholder — berdasarkan placeholder', async ({ page }) => {
await page.getByPlaceholder('Cari produk...').fill('laptop');
await page.getByPlaceholder(/email/i).fill('test@mail.com');
});
test('getByText — berdasarkan teks konten', async ({ page }) => {
// Exact text match
await page.getByText('Selamat Datang').click();
// Substring match
await page.getByText('Selamat', { exact: false }).click();
// Regex
await page.getByText(/total.*rp/i).click();
});
test('getByAltText — berdasarkan alt attribute', async ({ page }) => {
await page.getByAltText('Logo BeebaneLabs').click();
await page.getByAltText(/profil/i).click();
});
test('getByTitle — berdasarkan title attribute', async ({ page }) => {
await page.getByTitle('Klik untuk info').click();
});
test('getByTestId — berdasarkan data-testid', async ({ page }) => {
// Ini adalah fallback ketika locator lain tidak cocok
await page.getByTestId('submit-button').click();
await page.getByTestId('user-card-1').click();
});
test('locator chaining dan filtering', async ({ page }) => {
// Filter berdasarkan hasText
await page.getByRole('listitem')
.filter({ hasText: 'JavaScript' })
.getByRole('button')
.click();
// Filter berdasarkan hasNotText
await page.getByRole('listitem')
.filter({ hasNotText: 'Sold Out' })
.getByRole('button', { name: 'Beli' })
.click();
// Filter berdasarkan has (locator nested)
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Produk A' }) })
.getByRole('button')
.click();
// Nth element
await page.getByRole('listitem').nth(2).click();
await page.getByRole('listitem').first().click();
await page.getByRole('listitem').last().click();
// Count elements
const count = await page.getByRole('listitem').count();
expect(count).toBeGreaterThanOrEqual(3);
});
});
Prioritas locator dari yang terbaik:
getByRole()— paling robust, berdasarkan ARIA rolegetByLabel()— untuk form inputsgetByText()— untuk konten teksgetByPlaceholder()— fallback untuk inputgetByAltText()— untuk imagesgetByTestId()— fallback terakhir
Hindari: CSS selector langsung dan XPath — fragile dan berubah saat UI diupdate.
4. Actions & Assertions
User Actions
import { test, expect } from '@playwright/test';
test.describe('User Actions', () => {
test('form interactions', async ({ page }) => {
await page.goto('/form');
// ═══ Fill input ═══
await page.getByLabel('Nama').fill('Budi Santoso');
await page.getByLabel('Email').fill('budi@test.com');
// Clear and type
await page.getByLabel('Cari').clear();
await page.getByLabel('Cari').type('query baru', { delay: 50 });
// ═══ Click ═══
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Lihat Detail').click();
await page.getByTestId('close-modal').click();
// Double click
await page.getByTestId('editable-text').dblclick();
// Right click
await page.getByTestId('context-area').click({ button: 'right' });
// Shift + click
await page.getByRole('checkbox').click({ modifiers: ['Shift'] });
// ═══ Select dropdown ═══
await page.getByLabel('Kategori').selectOption('elektronik');
await page.getByLabel('Kota').selectOption({ label: 'Jakarta' });
await page.getByLabel('Multi-select').selectOption(['react', 'vue']);
// ═══ Checkbox & Radio ═══
await page.getByLabel('Setuju').check();
await page.getByLabel('Setuju').uncheck();
await page.getByLabel('Gold').check();
// ═══ File upload ═══
await page.getByLabel('Upload foto').setInputFiles('tests/fixtures/test.png');
await page.getByLabel('Upload foto').setInputFiles([
'tests/fixtures/img1.png',
'tests/fixtures/img2.png',
]);
// ═══ Hover ═══
await page.getByText('Menu').hover();
// ═══ Focus ═══
await page.getByLabel('Search').focus();
// ═══ Drag and drop ═══
await page.getByTestId('drag-source').dragTo(
page.getByTestId('drop-target')
);
// ═══ Scroll ═══
await page.getByTestId('footer').scrollIntoViewIfNeeded();
});
test('navigation', async ({ page }) => {
await page.goto('/');
await page.goto('/dashboard');
// Go back/forward
await page.goBack();
await page.goForward();
// Reload
await page.reload();
// Open new tab/page
const newPage = await page.context().newPage();
await newPage.goto('https://external.com');
await newPage.close();
// Intercept dialog
page.on('dialog', async (dialog) => {
expect(dialog.message()).toContain('Yakin?');
await dialog.accept();
});
await page.getByRole('button', { name: 'Hapus' }).click();
});
test('screenshots dan video', async ({ page }) => {
await page.goto('/');
// Full page screenshot
await page.screenshot({ path: 'full-page.png', fullPage: true });
// Element screenshot
await page.getByTestId('card').screenshot({ path: 'card.png' });
// Clip screenshot
await page.screenshot({
path: 'clipped.png',
clip: { x: 0, y: 0, width: 500, height: 300 },
});
});
});
Assertions
import { test, expect } from '@playwright/test';
test.describe('Playwright Assertions', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/dashboard');
});
test('assertions umum', async ({ page }) => {
// ═══ Page assertions ═══
await expect(page).toHaveTitle(/Dashboard/);
await expect(page).toHaveURL(/.*dashboard/);
// ═══ Element visibility ═══
await expect(page.getByTestId('card')).toBeVisible();
await expect(page.getByTestId('loading')).toBeHidden();
await expect(page.getByTestId('hidden')).toBeAttached(); // Ada di DOM
// ═══ Text content ═══
await expect(page.getByRole('heading')).toHaveText('Dashboard Utama');
await expect(page.getByRole('heading')).toContainText('Dashboard');
await expect(page.getByTestId('counter')).toHaveText('42');
// ═══ Multiple elements ═══
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('listitem')).toHaveText([
'Item 1',
'Item 2',
'Item 3',
]);
// ═══ Input values ═══
await expect(page.getByLabel('Email')).toHaveValue('budi@test.com');
await expect(page.getByLabel('Search')).toBeEmpty();
// ═══ CSS classes ═══
await expect(page.getByTestId('card')).toHaveClass(/active/);
await expect(page.getByTestId('card')).toHaveClass('card featured active');
await expect(page.getByTestId('card')).toHaveCSS('color', 'rgb(0, 0, 0)');
// ═══ Attributes ═══
await expect(page.getByRole('link')).toHaveAttribute('href', '/profile');
await expect(page.getByRole('link')).toHaveAttribute(
'href',
/profile/,
{ ignoreCase: true }
);
await expect(page.getByRole('button')).toHaveAttribute('disabled', '');
// ═══ State assertions ═══
await expect(page.getByLabel('Setuju')).toBeChecked();
await expect(page.getByLabel('Setuju')).not.toBeChecked();
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
await expect(page.getByLabel('Email')).toBeFocused();
await expect(page.getByLabel('Email')).toBeEditable();
await expect(page.getByLabel('Email')).toBeRequired();
// ═══ Timeout (override default) ═══
await expect(page.getByTestId('slow-element'))
.toBeVisible({ timeout: 15000 });
// ═══ Negation ═══
await expect(page.getByTestId('card')).not.toBeVisible();
await expect(page.getByTestId('counter')).not.toHaveText('0');
// ═══ Custom retry assertion ═══
await expect(async () => {
const response = await page.request.get('/api/status');
expect(response.status()).toBe(200);
}).toPass({ timeout: 10000, intervals: [1000] });
});
});
5. Fixtures & Test Hooks
Playwright menggunakan sistem fixtures yang powerful untuk setup test, dependency injection, dan sharing state antar test. Fixtures di Playwright berbeda dari Cypress — lebih fleksibel dan mendukung custom fixtures.
Built-in Test Hooks
import { test, expect } from '@playwright/test';
// ═══ Test Hooks ═══
test.beforeAll(async () => {
// Jalankan sekali sebelum semua test dalam file
console.log('Setup sekali untuk semua test');
});
test.beforeEach(async ({ page }) => {
// Jalankan sebelum setiap test
await page.goto('/');
});
test.afterEach(async ({ page }, testInfo) => {
// Jalankan setelah setiap test
// testInfo berisi informasi tentang test yang baru selesai
if (testInfo.status !== testInfo.expectedStatus) {
// Screenshot otomatis saat test gagal
await page.screenshot({
path: `screenshots/${testInfo.title}-failed.png`,
});
}
});
test.afterAll(async () => {
// Jalankan sekali setelah semua test
console.log('Cleanup setelah semua test');
});
// ═══ Test Structure ═══
test.describe('Suite: Homepage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('menampilkan judul', async ({ page }) => {
await expect(page).toHaveTitle(/BeebaneLabs/);
});
test('menampilkan navigasi', async ({ page }) => {
await expect(page.getByRole('navigation')).toBeVisible();
});
// Skip test
test.skip('fitur belum tersedia', async ({ page }) => {
// Test ini di-skip
});
// Only test (development only)
// test.only('fokus ke test ini', async ({ page }) => { ... });
// Conditional
test('hanya di Chromium', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'Hanya untuk Chromium');
// ...
});
});
Custom Fixtures
import { test as base, expect } from '@playwright/test';
// Definisikan custom fixtures
export const test = base.extend({
// Fixture sederhana
adminUser: async ({}, use) => {
const admin = {
nama: 'Admin Beebane',
email: 'admin@beebane.com',
role: 'admin',
};
await use(admin);
},
// Fixture dengan page interaction
loginPage: async ({ page }, use) => {
const login = {
async goto() {
await page.goto('/login');
},
async login(email, password) {
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Masuk' }).click();
await page.waitForURL('/dashboard');
},
};
await use(login);
},
// Fixture dengan API request
apiContext: async ({ playwright }, use) => {
const api = await playwright.request.newContext({
baseURL: 'http://localhost:3000/api',
extraHTTPHeaders: {
'Content-Type': 'application/json',
},
});
await use(api);
await api.dispose();
},
// Auto-use fixture (dijalankan otomatis)
autoLogin: [async ({ page }, use) => {
// Setup: login sebelum setiap test
await page.goto('/login');
await page.getByLabel('Email').fill('admin@beebane.com');
await page.getByLabel('Password').fill('Admin123!');
await page.getByRole('button', { name: 'Masuk' }).click();
await page.waitForURL('/dashboard');
await use(page);
// Teardown: bisa ditambahkan cleanup di sini
}, { auto: true }], // auto: true = jalankan otomatis
});
export { expect };
Menggunakan Custom Fixtures
import { test, expect } from './fixtures/my-fixtures';
test.describe('Dashboard', () => {
// autoLogin fixture sudah berjalan otomatis!
test('menampilkan data admin', async ({ page, adminUser }) => {
// autoLogin sudah login otomatis
await expect(page.getByText(adminUser.nama)).toBeVisible();
await expect(page.getByText('Admin Panel')).toBeVisible();
});
test('menggunakan API context', async ({ apiContext }) => {
const response = await apiContext.get('/users');
expect(response.ok()).toBeTruthy();
const users = await response.json();
expect(users.length).toBeGreaterThan(0);
});
test('menggunakan loginPage fixture', async ({ loginPage, page }) => {
await loginPage.login('user@test.com', 'User123!');
await expect(page).toHaveURL(/.*dashboard/);
});
});
Authentication Setup
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
// Login
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD);
await page.getByRole('button', { name: 'Masuk' }).click();
// Tunggu redirect ke dashboard
await page.waitForURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Simpan authentication state
await page.context().storageState({ path: authFile });
// Sekarang semua test yang depend on 'setup' akan
// otomatis memiliki auth state (tidak perlu login lagi)
});
6. Trace Viewer
Trace Viewer adalah tool visual debugging bawaan Playwright yang sangat powerful. Ini merekam seluruh eksekusi test — termasuk video, screenshot per langkah, DOM snapshot, network requests, dan console logs — sehingga Anda bisa menganalisis kegagalan test tanpa perlu menjalankan ulang.
Mengaktifkan Trace
// playwright.config.js
export default defineConfig({
use: {
// Opsi trace:
// 'off' → tidak merekam
// 'on' → selalu merekam
// 'retain-on-failure' → rekam semua, hapus yang berhasil
// 'on-first-retry' → rekam hanya saat retry (recommended)
trace: 'on-first-retry',
// Screenshot options
screenshot: 'only-on-failure', // 'off', 'on', 'only-on-failure'
// Video options
video: 'retain-on-failure', // 'off', 'on', 'retain-on-failure'
},
});
Menggunakan Trace Viewer
# Buka trace file di browser
npx playwright show-trace trace.zip
# Buka trace dari test results folder
npx playwright show-trace test-results/tests-login-chromium/trace.zip
# Record trace secara manual dalam test
# await page.context().tracing.start({ screenshots: true, snapshots: true });
# ... test code ...
# await page.context().tracing.stop({ path: 'trace.zip' });
# Buka HTML report (termasuk traces)
npx playwright show-report
# Record trace untuk bagian tertentu saja
await page.context().tracing.startChunk({ name: 'login-flow' });
// ... actions ...
await page.context().tracing.stopChunk({ path: 'login-trace.zip' });
Di dalam Trace Viewer Anda bisa melihat:
- Actions Timeline — daftar setiap aksi dengan durasi
- Before/After Snapshots — DOM sebelum dan sesudah setiap aksi
- Video Playback — rekaman video dari seluruh test
- Network Tab — semua HTTP requests dan responses
- Console Tab — semua console.log, warn, error
- Source Tab — kode test yang dieksekusi
- Metadata — browser, viewport, timing info
7. API Testing
Playwright memiliki fitur API Testing built-in yang memungkinkan Anda menguji backend API tanpa browser. Ini sangat berguna untuk testing endpoint API, meng-setup data test, dan verifikasi data yang disimpan server.
import { test, expect } from '@playwright/test';
// API Testing tanpa browser
test.describe('API Testing', () => {
const BASE_URL = 'http://localhost:3000/api';
test('GET /api/users — mendapatkan daftar user', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toBeInstanceOf(Array);
expect(body.length).toBeGreaterThan(0);
expect(body[0]).toHaveProperty('id');
expect(body[0]).toHaveProperty('nama');
expect(body[0]).toHaveProperty('email');
});
test('POST /api/users — membuat user baru', async ({ request }) => {
const response = await request.post(`${BASE_URL}/users`, {
data: {
nama: 'User Test',
email: 'test@playwright.com',
role: 'user',
},
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.nama).toBe('User Test');
expect(user.email).toBe('test@playwright.com');
expect(user).toHaveProperty('id');
});
test('PUT /api/users/:id — update user', async ({ request }) => {
// Buat user dulu
const createResponse = await request.post(`${BASE_URL}/users`, {
data: { nama: 'Update Test', email: 'update@test.com' },
});
const created = await createResponse.json();
// Update
const updateResponse = await request.put(`${BASE_URL}/users/${created.id}`, {
data: { nama: 'Updated Name' },
});
expect(updateResponse.ok()).toBeTruthy();
const updated = await updateResponse.json();
expect(updated.nama).toBe('Updated Name');
});
test('DELETE /api/users/:id — hapus user', async ({ request }) => {
const createResponse = await request.post(`${BASE_URL}/users`, {
data: { nama: 'Delete Test', email: 'delete@test.com' });
const created = await createResponse.json();
const deleteResponse = await request.delete(
`${BASE_URL}/users/${created.id}`
);
expect(deleteResponse.status()).toBe(204);
// Verifikasi sudah terhapus
const getResponse = await request.get(
`${BASE_URL}/users/${created.id}`
);
expect(getResponse.status()).toBe(404);
});
test('API dengan authentication', async ({ request }) => {
// Login dulu untuk mendapatkan token
const loginResponse = await request.post(`${BASE_URL}/auth/login`, {
data: {
email: 'admin@beebane.com',
password: 'Admin123!',
},
});
const { token } = await loginResponse.json();
// Gunakan token untuk request berikutnya
const protectedResponse = await request.get(`${BASE_URL}/admin/stats`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
expect(protectedResponse.ok()).toBeTruthy();
});
});
// API testing dalam E2E test (kombinasi browser + API)
test.describe('E2E + API', () => {
test('buat artikel via API, verifikasi di browser', async ({
page,
request,
}) => {
// Setup data via API (lebih cepat dari UI)
const response = await request.post(`${BASE_URL}/articles`, {
data: {
judul: 'Artikel dari API',
konten: 'Konten test',
tag: ['test'],
},
});
const article = await response.json();
// Verifikasi di browser
await page.goto(`/articles/${article.id}`);
await expect(page.getByRole('heading')).toHaveText('Artikel dari API');
await expect(page.getByText('Konten test')).toBeVisible();
});
});
8. Parallel Execution
Salah satu keunggulan terbesar Playwright dibanding Cypress adalah parallel execution built-in. Playwright bisa menjalankan test secara paralel menggunakan workers — tanpa perlu tool atau layanan tambahan.
Konfigurasi Parallel
// playwright.config.js
export default defineConfig({
// ═══ Parallel execution ═══
fullyParallel: true, // Semua test berjalan paralel
// Jumlah workers (default: CPU cores / 2)
workers: process.env.CI ? 2 : 4,
// Atau tentukan persentase CPU
// workers: '50%', // Gunakan 50% dari CPU cores
// Test sharding (untuk CI parallelization)
// Shard 1 dari 3: npx playwright test --shard=1/3
// Shard 2 dari 3: npx playwright test --shard=2/3
// Shard 3 dari 3: npx playwright test --shard=3/3
});
Test Ordering & Dependencies
import { test, expect } from '@playwright/test';
// Project dependencies (di config):
// projects: [
// { name: 'setup', testMatch: /.*\.setup\.js/ },
// { name: 'chromium', dependencies: ['setup'] },
// ]
// Dalam satu file, test berjalan berurutan
test.describe.serial('Tests yang harus berurutan', () => {
test('step 1: buat data', async ({ page }) => {
await page.goto('/create');
// ...
});
test('step 2: verifikasi data', async ({ page }) => {
// Pastikan step 1 sudah selesai
await page.goto('/verify');
// ...
});
});
// Test paralel (default)
test.describe.parallel('Tests yang bisa paralel', () => {
test('test A', async ({ page }) => {
// Bisa berjalan bersamaan dengan test B
});
test('test B', async ({ page }) => {
// Bisa berjalan bersamaan dengan test A
});
});
// Retry specific test
test('test yang flaky', async ({ page }) => {
test.retry(3); // Retry 3 kali jika gagal
// ...
});
Default: Semua test berjalan paralel. Gunakan test.describe.serial() ketika test memiliki dependency berurutan (misalnya: create → update → delete). Hindari serial tests sebisa mungkin — mereka 3-5x lebih lambat dari paralel.
9. CI/CD Integration
GitHub Actions
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox webkit
- name: Build application
run: npm run build
- name: Run Playwright tests (shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.shardIndex }}
path: playwright-report/
retention-days: 14
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.shardIndex }}
path: test-results/
retention-days: 7
Perintah Berguna
# Jalankan semua test npx playwright test # Jalankan test file tertentu npx playwright test tests/login.spec.js # Jalankan test dengan nama tertentu npx playwright test -g "login berhasil" # Jalankan dengan browser tertentu npx playwright test --project=chromium npx playwright test --project=firefox # Jalankan secara paralel dengan workers npx playwright test --workers=4 # Sharding untuk CI parallel npx playwright test --shard=1/3 # Debug mode (headed, step-by-step) npx playwright test --debug # UI mode (visual test runner) npx playwright test --ui # Codegen: rekam interaksi untuk generate test code npx playwright codegen http://localhost:3000 # Codegen dengan device emulation npx playwright codegen --device="iPhone 13" http://localhost:3000 # Buka HTML report npx playwright show-report # Buka trace viewer npx playwright show-trace trace.zip # List semua test tanpa menjalankan npx playwright test --list
Playwright Codegen merekam interaksi browser dan menghasilkan kode test secara otomatis:
npx playwright codegen http://localhost:3000
Ini membuka browser dan panel kode. Semua klik, ketik, dan navigasi akan otomatis diubah menjadi kode test. Sangat berguna untuk bootstrap test suite awal atau mempelajari API Playwright.
10. Quiz Pemahaman
Uji pemahaman Anda tentang Playwright Testing:
1. Locator mana yang PALING direkomendasikan Playwright?
2. Apa keunggulan utama Trace Viewer?
3. Bagaimana Playwright menjalankan test secara paralel?
4. Apa fungsi storageState di Playwright?
5. Browser apa SAJA yang didukung Playwright?