Web Development

Playwright: Modern End-to-End Testing

Tutorial lengkap Playwright Testing — locators, fixtures, trace viewer, parallel execution, API testing, codegen, dan CI/CD integration untuk testing otomatis modern

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-browserChromium, Firefox, WebKit — test di semua browser utama
Auto-waitingMenunggu elemen siap secara otomatis sebelum berinteraksi
Trace ViewerVisual debugging tool — lihat video, screenshot, dan DOM di setiap langkah
CodegenGenerate test code otomatis dari rekaman interaksi browser
Parallel ExecutionMenjalankan test secara paralel bawaan tanpa tool tambahan
Multi-languageJavaScript, TypeScript, Python, Java, C# (.NET)
API TestingBuilt-in API request context untuk testing backend
Network InterceptionMock dan modify network requests/responses
Mobile EmulationEmulate 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
Diagram: Arsitektur Playwright
┌──────────────────────────────────────────────────────────┐
│                  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

Bash
# 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

JavaScript — playwright.config.js
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

File Structure
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

JavaScript — tests/locators.spec.js
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);
  });
});
💡 Best Practice Locators

Prioritas locator dari yang terbaik:

  1. getByRole() — paling robust, berdasarkan ARIA role
  2. getByLabel() — untuk form inputs
  3. getByText() — untuk konten teks
  4. getByPlaceholder() — fallback untuk input
  5. getByAltText() — untuk images
  6. getByTestId() — fallback terakhir

Hindari: CSS selector langsung dan XPath — fragile dan berubah saat UI diupdate.

4. Actions & Assertions

User Actions

JavaScript — tests/actions.spec.js
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

JavaScript — tests/assertions.spec.js
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

JavaScript — tests/hooks.spec.js
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

JavaScript — tests/fixtures/my-fixtures.js
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

JavaScript — tests/dashboard.spec.js
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

JavaScript — tests/auth.setup.js
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

JavaScript — Trace Configuration
// 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

Bash — Trace Viewer Commands
# 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' });
💡 Trace Viewer Features

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.

JavaScript — tests/api-testing.spec.js
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

JavaScript — Parallel Configuration
// 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

JavaScript — Test 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
  // ...
});
📋 Parallel vs Serial

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

YAML — .github/workflows/playwright.yml
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

Bash — Playwright Commands
# 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
💡 Codegen: Record & Generate

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?

🔍 Zoom
100%
🎨 Tema