Kuasai Playwright dari konsep inti hingga produksi. Pelajari end-to-end testing, cross-browser automation, dan visual regression testing. Bangun complete e-commerce web application dengan React dan comprehensive Playwright test suites covering user flows, API mocking, dan CI/CD integration.

Manual testing adalah slow, error-prone, dan tidak scale. Ketika applications grow lebih complex, testing setiap user interaction manually menjadi impossible. End-to-end testing mengotomatisasi ini dengan simulating real user behavior across browsers dan devices.
Playwright adalah modern browser automation framework yang membuat E2E testing accessible, reliable, dan fast. Digunakan oleh companies seperti Microsoft, Google, dan thousands dari development teams, Playwright enable Anda untuk test web applications across Chrome, Firefox, Safari, dan Edge simultaneously.
Dalam artikel ini, kita akan mengeksplorasi arsitektur Playwright, memahami E2E testing fundamentals, dan membangun production-ready e-commerce web application dengan React yang kita test comprehensively dengan realistic user scenarios, API mocking, dan visual regression testing.
Traditional testing approaches memiliki significant limitations:
Manual Testing: Time-consuming, error-prone, dan tidak scale.
Selenium Limitations: Flaky tests, slow execution, complex setup.
Single Browser: Sulit untuk test across multiple browsers simultaneously.
No Visual Testing: Tidak bisa detect UI regressions automatically.
Poor Developer Experience: Complex APIs, difficult debugging.
Slow Feedback: Tests memerlukan terlalu lama untuk run.
Maintenance Burden: Tests break easily dengan UI changes.
Playwright dibangun oleh Microsoft untuk solve problems ini:
Fast & Reliable: Optimized untuk speed dan stability.
Multi-Browser: Test Chrome, Firefox, Safari, Edge simultaneously.
Developer-Friendly: Simple, intuitive API.
Visual Testing: Built-in screenshot dan visual regression testing.
Network Control: Mock APIs dan network requests.
Debugging: Excellent debugging tools dan inspector.
CI/CD Ready: Integrates seamlessly dengan pipelines.
Open Source: Free dan community-driven.
Browser: Chromium, Firefox, atau WebKit instance.
Context: Isolated browser session dengan separate cookies dan storage.
Page: Single tab atau window dalam context.
Locator: Way untuk find elements pada page.
Action: User interaction seperti click, type, atau navigate.
Assertion: Validation bahwa something adalah true.
Fixture: Reusable test setup dan teardown.
Trace: Recording dari test execution untuk debugging.
Test Script → Playwright Engine → Browser Protocol → Browser Instance → Web ApplicationPlaywright API (JavaScript/Python/Java/C#)
↓
Playwright Server (Node.js)
↓
Browser Protocol (WebSocket)
↓
Browser Instance (Chrome/Firefox/Safari/Edge)
↓
Web ApplicationPlaywright menggunakan browser protocol untuk direct communication, membuat lebih fast dan reliable daripada Selenium.
Automate browser interactions programmatically.
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Navigate ke URL
await page.goto('https://example.com');
// Interact dengan elements
await page.click('button#submit');
await page.fill('input[name="email"]', 'user@example.com');
await page.type('input[name="password"]', 'password123');
// Wait untuk navigation
await page.waitForNavigation();
// Take screenshot
await page.screenshot({ path: 'screenshot.png' });
await browser.close();Use Cases:
Find dan interact dengan elements reliably.
import { test, expect } from '@playwright/test';
test('using locators', async ({ page }) => {
// CSS selector
await page.locator('button.submit').click();
// XPath
await page.locator('//button[text()="Submit"]').click();
// Text content
await page.locator('text=Submit').click();
// Role-based (recommended)
await page.locator('role=button[name="Submit"]').click();
// Chaining locators
await page.locator('form').locator('input[name="email"]').fill('user@example.com');
// Get by label
await page.getByLabel('Email').fill('user@example.com');
// Get by placeholder
await page.getByPlaceholder('Enter email').fill('user@example.com');
// Get by role
await page.getByRole('button', { name: 'Submit' }).click();
});Use Cases:
Verify application state dan behavior.
import { test, expect } from '@playwright/test';
test('assertions', async ({ page }) => {
await page.goto('https://example.com');
// URL assertions
expect(page).toHaveURL('https://example.com');
// Text assertions
expect(page.locator('h1')).toContainText('Welcome');
// Visibility assertions
expect(page.locator('button')).toBeVisible();
expect(page.locator('.hidden')).toBeHidden();
// Enabled/Disabled
expect(page.locator('input')).toBeEnabled();
expect(page.locator('button')).toBeDisabled();
// Count assertions
expect(page.locator('li')).toHaveCount(5);
// Value assertions
expect(page.locator('input')).toHaveValue('expected value');
// Attribute assertions
expect(page.locator('a')).toHaveAttribute('href', '/page');
// Class assertions
expect(page.locator('div')).toHaveClass('active');
// CSS assertions
expect(page.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
});Use Cases:
Handle asynchronous operations reliably.
import { test, expect } from '@playwright/test';
test('waiting strategies', async ({ page }) => {
// Wait untuk navigation
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/next-page"]'),
]);
// Wait untuk element
await page.waitForSelector('button.loaded');
// Wait untuk function
await page.waitForFunction(() => {
return document.querySelectorAll('li').length > 5;
});
// Wait untuk specific condition
await expect(page.locator('.loading')).toBeHidden();
// Wait dengan timeout
await page.waitForSelector('button', { timeout: 5000 });
// Auto-waiting (built-in)
// Playwright automatically waits untuk elements menjadi actionable
await page.click('button'); // Waits untuk button menjadi visible dan enabled
});Use Cases:
Control dan mock network requests.
import { test, expect } from '@playwright/test';
test('network mocking', async ({ page }) => {
// Mock API response
await page.route('**/api/products', (route) => {
route.abort('blockedbyclient');
});
// Mock dengan custom response
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]),
});
});
// Intercept dan modify request
await page.route('**/api/checkout', (route) => {
const request = route.request();
route.continue({
postData: JSON.stringify({
...JSON.parse(request.postData()),
testMode: true,
}),
});
});
// Log semua requests
page.on('request', (request) => {
console.log(request.method(), request.url());
});
// Log semua responses
page.on('response', (response) => {
console.log(response.status(), response.url());
});
await page.goto('https://example.com');
});Use Cases:
Test across multiple browsers simultaneously.
import { test, expect, chromium, firefox, webkit } from '@playwright/test';
test('cross-browser', async () => {
for (const browserType of [chromium, firefox, webkit]) {
const browser = await browserType.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
expect(page).toHaveURL('https://example.com');
await browser.close();
}
});
// Atau use test matrix
test.describe.parallel('cross-browser suite', () => {
test('chromium', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
test('firefox', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
test('webkit', async ({ browser }) => {
const page = await browser.newPage();
await page.goto('https://example.com');
});
});Use Cases:
Detect UI changes automatically.
import { test, expect } from '@playwright/test';
test('visual regression', async ({ page }) => {
await page.goto('https://example.com');
// Full page screenshot
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
// Element screenshot
expect(await page.locator('header').screenshot()).toMatchSnapshot('header.png');
// Dengan options
expect(await page.screenshot({
fullPage: true,
mask: [page.locator('.dynamic-content')],
})).toMatchSnapshot('page-masked.png');
// Update snapshots
// Run dengan: npx playwright test --update-snapshots
});Use Cases:
Reusable test setup dan teardown.
import { test as base, expect } from '@playwright/test';
// Define custom fixture
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Setup
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await page.waitForNavigation();
// Use fixture
await use(page);
// Teardown
await page.goto('https://example.com/logout');
},
apiClient: async ({}, use) => {
const client = {
async get(url) {
const response = await fetch(url);
return response.json();
},
};
await use(client);
},
});
test('using fixtures', async ({ authenticatedPage, apiClient }) => {
// authenticatedPage sudah logged in
await authenticatedPage.goto('https://example.com/dashboard');
expect(authenticatedPage).toHaveURL('**/dashboard');
// Use API client
const data = await apiClient.get('https://api.example.com/user');
expect(data).toHaveProperty('id');
});
export { test, expect };Use Cases:
Powerful debugging tools untuk test failures.
import { test, expect } from '@playwright/test';
test('debugging', async ({ page }) => {
// Enable trace recording
await page.context().tracing.start({ screenshots: true, snapshots: true });
await page.goto('https://example.com');
await page.click('button');
// Stop dan save trace
await page.context().tracing.stop({ path: 'trace.zip' });
// Pause execution untuk debugging
// await page.pause();
// Log information
console.log('Current URL:', page.url());
console.log('Page title:', await page.title());
// Get page content
const content = await page.content();
console.log('HTML:', content);
});Use Cases:
Run tests efficiently across multiple workers.
import { test, expect } from '@playwright/test';
// Configure parallel execution
export const config = {
fullyParallel: true,
workers: 4,
retries: 2,
timeout: 30000,
};
test.describe.parallel('parallel tests', () => {
test('test 1', async ({ page }) => {
await page.goto('https://example.com');
});
test('test 2', async ({ page }) => {
await page.goto('https://example.com');
});
test('test 3', async ({ page }) => {
await page.goto('https://example.com');
});
});
// Sharding untuk CI/CD
// Run dengan: npx playwright test --shard=1/3Use Cases:
Sekarang mari kita bangun production-ready e-commerce web application dengan React yang kita test comprehensively. Application include:
npx create-react-app ecommerce-app
cd ecommerce-app
npm install axios react-router-dom zustand
npm install -D @playwright/test
npx playwright installimport axios from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000/api';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token ke requests
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('authToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
export const productApi = {
getAll: (limit = 20, offset = 0) =>
apiClient.get('/products', { params: { limit, offset } }),
getById: (id) => apiClient.get(`/products/${id}`),
search: (query) => apiClient.get('/products/search', { params: { q: query } }),
};
export const cartApi = {
getCart: () => apiClient.get('/cart'),
addItem: (productId, quantity) =>
apiClient.post('/cart/add', { productId, quantity }),
removeItem: (productId) => apiClient.delete(`/cart/items/${productId}`),
updateQuantity: (productId, quantity) =>
apiClient.put(`/cart/items/${productId}`, { quantity }),
clear: () => apiClient.post('/cart/clear'),
};
export const orderApi = {
create: (items) => apiClient.post('/orders', { items }),
getById: (id) => apiClient.get(`/orders/${id}`),
getAll: () => apiClient.get('/orders'),
processPayment: (orderId, paymentDetails) =>
apiClient.post(`/orders/${orderId}/payment`, paymentDetails),
};
export const authApi = {
login: (email, password) =>
apiClient.post('/auth/login', { email, password }),
register: (email, password, name) =>
apiClient.post('/auth/register', { email, password, name }),
logout: () => apiClient.post('/auth/logout'),
getCurrentUser: () => apiClient.get('/auth/me'),
};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 ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'npm start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});import { test, expect } from '@playwright/test';
test.describe('Products Page', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display products list', async ({ page }) => {
// Wait untuk products load
await expect(page.locator('[data-testid="products-page"]')).toBeVisible();
// Check products displayed
const products = page.locator('[data-testid^="product-"]');
const count = await products.count();
expect(count).toBeGreaterThan(0);
});
test('should add product to cart', async ({ page }) => {
// Click add to cart button
await page.click('[data-testid="add-to-cart-1"]');
// Verify cart updated
const cartBadge = page.locator('[data-testid="cart-badge"]');
await expect(cartBadge).toContainText('1');
});
test('should filter products by search', async ({ page }) => {
// Type dalam search
await page.fill('[data-testid="search-input"]', 'laptop');
// Wait untuk results
await page.waitForTimeout(500);
// Verify filtered results
const products = page.locator('[data-testid^="product-"]');
const count = await products.count();
expect(count).toBeGreaterThan(0);
});
test('should display product details', async ({ page }) => {
// Click pada product
await page.click('[data-testid="product-1"]');
// Wait untuk details page
await page.waitForURL('**/products/1');
// Verify details displayed
await expect(page.locator('[data-testid="product-name"]')).toBeVisible();
await expect(page.locator('[data-testid="product-price"]')).toBeVisible();
await expect(page.locator('[data-testid="product-description"]')).toBeVisible();
});
});import { test, expect } from '@playwright/test';
test.describe('Shopping Cart', () => {
test('should add multiple items to cart', async ({ page }) => {
await page.goto('/');
// Add first product
await page.click('[data-testid="add-to-cart-1"]');
await expect(page.locator('[data-testid="cart-badge"]')).toContainText('1');
// Add second product
await page.click('[data-testid="add-to-cart-2"]');
await expect(page.locator('[data-testid="cart-badge"]')).toContainText('2');
// Navigate ke cart
await page.click('[data-testid="cart-link"]');
// Verify items dalam cart
await expect(page.locator('[data-testid="cart-item-1"]')).toBeVisible();
await expect(page.locator('[data-testid="cart-item-2"]')).toBeVisible();
});
test('should remove item from cart', async ({ page }) => {
await page.goto('/');
// Add product
await page.click('[data-testid="add-to-cart-1"]');
// Go ke cart
await page.click('[data-testid="cart-link"]');
// Remove item
await page.click('[data-testid="remove-1"]');
// Verify empty cart message
await expect(page.locator('[data-testid="empty-cart"]')).toBeVisible();
});
test('should calculate correct total', async ({ page }) => {
await page.goto('/');
// Add products dengan known prices
await page.click('[data-testid="add-to-cart-1"]'); // $100
await page.click('[data-testid="add-to-cart-2"]'); // $50
// Go ke cart
await page.click('[data-testid="cart-link"]');
// Verify total
await expect(page.locator('[data-testid="cart-total"]')).toContainText('$150');
});
});import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
// Add items ke cart
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="add-to-cart-2"]');
});
test('should complete checkout successfully', async ({ page }) => {
// Go ke cart
await page.click('[data-testid="cart-link"]');
// Click checkout
await page.click('[data-testid="checkout-btn"]');
// Wait untuk checkout page
await page.waitForURL('**/checkout');
// Fill form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="card-input"]', '4111111111111111');
await page.fill('[data-testid="expiry-input"]', '12/25');
await page.fill('[data-testid="cvv-input"]', '123');
// Submit
await page.click('[data-testid="submit-btn"]');
// Wait untuk confirmation
await page.waitForURL('**/order-confirmation/**');
// Verify confirmation
await expect(page.locator('[data-testid="confirmation-message"]')).toContainText('Thank you');
});
test('should handle payment errors', async ({ page }) => {
// Mock payment failure
await page.route('**/api/orders/*/payment', (route) => {
route.abort('failed');
});
// Go ke cart
await page.click('[data-testid="cart-link"]');
// Click checkout
await page.click('[data-testid="checkout-btn"]');
// Fill form
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="card-input"]', '4111111111111111');
await page.fill('[data-testid="expiry-input"]', '12/25');
await page.fill('[data-testid="cvv-input"]', '123');
// Submit
await page.click('[data-testid="submit-btn"]');
// Verify error message
await expect(page.locator('[data-testid="error-message"]')).toContainText('Payment failed');
});
});import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('homepage should match snapshot', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
});
test('products page should match snapshot', async ({ page }) => {
await page.goto('/products');
await page.waitForLoadState('networkidle');
expect(await page.screenshot()).toMatchSnapshot('products-page.png');
});
test('cart should match snapshot', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="cart-link"]');
expect(await page.screenshot()).toMatchSnapshot('cart-page.png');
});
test('checkout form should match snapshot', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="add-to-cart-1"]');
await page.click('[data-testid="cart-link"]');
await page.click('[data-testid="checkout-btn"]');
expect(await page.screenshot()).toMatchSnapshot('checkout-page.png');
});
});# Install dependencies
npm install
# Run semua tests
npx playwright test
# Run specific test file
npx playwright test tests/e2e/products.spec.ts
# Run tests dalam headed mode (see browser)
npx playwright test --headed
# Run tests dalam debug mode
npx playwright test --debug
# Run tests dengan UI mode
npx playwright test --ui
# Run tests untuk specific browser
npx playwright test --project=chromium
# Run tests dalam parallel
npx playwright test --workers=4
# Update visual snapshots
npx playwright test --update-snapshots
# Generate HTML report
npx playwright show-report// ❌ Wrong - unreliable
await page.waitForTimeout(2000);
await page.click('button');
// ✅ Correct - wait untuk element
await page.waitForSelector('button');
await page.click('button');
// ✅ Better - use locators dengan auto-waiting
await page.locator('button').click();// ❌ Wrong - breaks dengan UI changes
await page.click('div > div > button:nth-child(3)');
// ✅ Correct - semantic selectors
await page.getByRole('button', { name: 'Submit' }).click();
// ✅ Good - data-testid
await page.click('[data-testid="submit-btn"]');// ❌ Wrong - race condition
await page.click('a[href="/next"]');
await page.locator('h1').click(); // May fail jika navigation tidak complete
// ✅ Correct - wait untuk navigation
await Promise.all([
page.waitForNavigation(),
page.click('a[href="/next"]'),
]);
await page.locator('h1').click();// ❌ Wrong - tests depend pada each other
test('login', async ({ page }) => {
await page.goto('/login');
// ... login code
});
test('dashboard', async ({ page }) => {
// Assumes previous test logged in
await page.goto('/dashboard');
});
// ✅ Correct - setiap test adalah independent
test('dashboard after login', async ({ page }) => {
await page.goto('/login');
// ... login code
await page.goto('/dashboard');
// ... assertions
});// ❌ Wrong - depends pada external service
test('checkout', async ({ page }) => {
await page.goto('/checkout');
await page.click('button[type="submit"]');
// Test fails jika payment service down
});
// ✅ Correct - mock API
test('checkout', async ({ page }) => {
await page.route('**/api/payment', (route) => {
route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
});
await page.goto('/checkout');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('**/confirmation');
});Prefer role-based dan label-based locators over CSS selectors.
// ✅ Best - semantic
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('user@example.com');
await page.getByPlaceholder('Enter password').fill('password');
// Good - data-testid
await page.locator('[data-testid="submit-btn"]').click();
// Avoid - fragile selectors
await page.locator('div > button:nth-child(3)').click();Group related tests dan use descriptive names.
test.describe('User Authentication', () => {
test.describe('Login', () => {
test('should login dengan valid credentials', async ({ page }) => {
// ...
});
test('should show error dengan invalid credentials', async ({ page }) => {
// ...
});
});
test.describe('Registration', () => {
test('should register new user', async ({ page }) => {
// ...
});
});
});Reuse common setup logic dengan fixtures.
const test = base.extend({
authenticatedPage: async ({ page }, use) => {
// Setup
await loginUser(page);
await use(page);
// Teardown
await logoutUser(page);
},
});
test('dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
// Test authenticated page
});Isolate tests dengan mocking APIs dan external services.
test('checkout', async ({ page }) => {
// Mock payment API
await page.route('**/api/payment', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ transactionId: '12345' }),
});
});
// Mock shipping API
await page.route('**/api/shipping', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ estimatedDays: 3 }),
});
});
// Test checkout flow
await page.goto('/checkout');
// ...
});Focus pada complete user journeys, bukan individual components.
test('complete purchase flow', async ({ page }) => {
// Browse products
await page.goto('/products');
await page.click('[data-testid="product-1"]');
// Add ke cart
await page.click('[data-testid="add-to-cart"]');
// Checkout
await page.click('[data-testid="cart-link"]');
await page.click('[data-testid="checkout-btn"]');
// Complete purchase
await page.fill('[data-testid="email"]', 'user@example.com');
await page.click('[data-testid="submit"]');
// Verify confirmation
await expect(page).toHaveURL('**/confirmation');
});Catch unintended UI changes automatically.
test('homepage layout', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// Compare dengan baseline
expect(await page.screenshot()).toMatchSnapshot('homepage.png');
});Run tests automatically dalam pipeline Anda.
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/Playwright transforms frontend testing dari manual, error-prone work menjadi automated, reliable validation. Memahami E2E testing fundamentals—locators, assertions, waiting, dan mocking—enable Anda untuk build comprehensive test suites.
E-commerce application example demonstrate production patterns:
Key takeaways:
Next steps:
Playwright makes E2E testing accessible dan practical. Master it, dan Anda akan build applications dengan confidence, knowing bahwa user workflows adalah tested dan validated automatically.