Playwright ile Cross-Browser Otomatik Test: Kurulum ve Kullanım Rehberi

Modern web uygulamaları artık onlarca farklı tarayıcı ve cihaz üzerinde çalışmak zorunda. Kullanıcılarınızın bir kısmı Chrome kullanırken diğerleri Firefox, Safari veya Edge kullanıyor. Bu çeşitlilik, test süreçlerini ciddi ölçüde karmaşıklaştırıyor. İşte tam bu noktada Microsoft’un geliştirdiği Playwright devreye giriyor. Selenium’un yerini almaya aday bu güçlü araç, tek bir API ile Chromium, Firefox ve WebKit tarayıcılarını test etmenizi sağlıyor. Bu yazıda Playwright’ı sıfırdan kurup production ortamında nasıl kullanacağınızı, gerçek dünya senaryolarıyla birlikte anlatacağım.

Playwright Nedir ve Neden Kullanmalıyız?

Playwright, Microsoft tarafından 2020 yılında açık kaynak olarak yayınlanan bir end-to-end test framework’üdür. Selenium’dan en önemli farkı, modern tarayıcı API’larını doğrudan kullandığı için çok daha hızlı ve güvenilir olmasıdır.

Playwright’ın öne çıkan özellikleri şunlar:

  • Cross-browser desteği: Chromium (Chrome, Edge), Firefox ve WebKit (Safari) tek API ile test edilebilir
  • Auto-wait mekanizması: Elementlerin hazır olmasını otomatik bekler, sleep() yazmak zorunda kalmazsınız
  • Network intercepting: HTTP isteklerini yakalayıp mock’layabilirsiniz
  • Paralel test çalıştırma: Testler varsayılan olarak paralel koşar, CI/CD sürelerinizi dramatik düşürür
  • Trace viewer: Başarısız testleri adım adım inceleyebileceğiniz görsel araç
  • TypeScript desteği: Kutudan çıkar çıkmaz TypeScript ile çalışır

Eğer hâlâ Selenium veya Puppeteer kullanıyorsanız, özellikle büyük projelerde Playwright’a geçmeyi ciddi ciddi değerlendirmenizi öneririm.

Kurulum ve Ortam Hazırlığı

Node.js 16 veya üzeri bir versiyon gerekiyor. Önce Node.js versiyonunuzu kontrol edin:

node --version
npm --version

Yeni bir proje oluşturup Playwright’ı kuralım. Playwright’ın kendi init scripti sayesinde kurulum oldukça kolay:

mkdir playwright-demo && cd playwright-demo
npm init -y
npm init playwright@latest

Bu komut size birkaç soru sorar. Ben genellikle şu şekilde cevaplıyorum:

  • Test dosyalarının nereye konacağı: tests
  • GitHub Actions workflow eklensin mi: y (CI/CD için şart)
  • Playwright tarayıcıları indirilsin mi: y

Kurulum tamamlandıktan sonra proje yapısı şöyle görünür:

ls -la
# playwright.config.ts
# tests/
#   example.spec.ts
# tests-examples/
# package.json
# .github/workflows/playwright.yml

Tarayıcı binary’lerini ayrıca yüklemek isterseniz:

# Tüm tarayıcıları yükle
npx playwright install

# Sadece belirli tarayıcıları yükle
npx playwright install chromium firefox

# Sistem bağımlılıklarını da yükle (Linux sunucularda önemli)
npx playwright install --with-deps

Linux sunucularda --with-deps flagini mutlaka kullanın. Aksi takdirde eksik shared library hatalarıyla saatlerce uğraşabilirsiniz.

Playwright Konfigürasyonu

Kurulum sonrası oluşan playwright.config.ts dosyasını ihtiyaçlarınıza göre düzenlemeniz gerekiyor. İşte production’da kullandığım gerçekçi bir konfigürasyon:

cat playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['junit', { outputFile: 'test-results/results.xml' }],
    ['list']
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
    actionTimeout: 10000,
    navigationTimeout: 30000,
  },
  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'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 13'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

Bu konfigürasyonun önemli noktaları:

  • fullyParallel: true: Her test dosyası paralel çalışır
  • retries: 2: CI ortamında başarısız testler 2 kez yeniden denenir, flaky testlerin önüne geçer
  • trace: 'on-first-retry': Sadece başarısız testler için trace kaydeder, disk alanını verimli kullanır
  • webServer: Testler başlamadan önce uygulamanızı otomatik ayağa kaldırır

İlk Testlerinizi Yazmak

Playwright’ın sözdizimi oldukça sezgisel. Temel bir login testi yazalım:

// tests/auth/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login Sayfası', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('başarılı giriş yapılmalı', async ({ page }) => {
    await page.getByLabel('E-posta').fill('[email protected]');
    await page.getByLabel('Şifre').fill('gizli123');
    await page.getByRole('button', { name: 'Giriş Yap' }).click();

    // Dashboard'a yönlendirilmeli
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Hoş geldiniz')).toBeVisible();
  });

  test('yanlış şifre ile giriş reddedilmeli', async ({ page }) => {
    await page.getByLabel('E-posta').fill('[email protected]');
    await page.getByLabel('Şifre').fill('yanlis-sifre');
    await page.getByRole('button', { name: 'Giriş Yap' }).click();

    await expect(page.getByText('E-posta veya şifre hatalı')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('boş form gönderilmemeli', async ({ page }) => {
    await page.getByRole('button', { name: 'Giriş Yap' }).click();

    await expect(page.getByText('E-posta zorunludur')).toBeVisible();
    await expect(page.getByText('Şifre zorunludur')).toBeVisible();
  });
});

getByLabel, getByRole gibi lokator metodları Selenium’daki xpath ya da CSS selector karmaşasından sizi kurtarır. Bu metodlar accessibility tree’yi kullandığı için testleriniz daha dayanıklı olur.

Page Object Model Kullanımı

Gerçek projelerde test kodunu organize etmek için Page Object Model (POM) pattern’ini kullanmak şart. Her sayfa için ayrı bir sınıf oluşturuyoruz:

// tests/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('E-posta');
    this.passwordInput = page.getByLabel('Şifre');
    this.submitButton = page.getByRole('button', { name: 'Giriş Yap' });
    this.errorMessage = page.getByRole('alert');
  }

  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();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }
}

// tests/pages/DashboardPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class DashboardPage {
  readonly page: Page;
  readonly welcomeMessage: Locator;
  readonly userMenu: Locator;

  constructor(page: Page) {
    this.page = page;
    this.welcomeMessage = page.getByRole('heading', { name: /Hoş geldiniz/ });
    this.userMenu = page.getByTestId('user-menu');
  }

  async expectLoaded() {
    await expect(this.page).toHaveURL('/dashboard');
    await expect(this.welcomeMessage).toBeVisible();
  }
}

Bu page object’leri kullanan test dosyası çok daha temiz görünür:

// tests/auth/login-pom.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

test('POM ile başarılı giriş', async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardPage(page);

  await loginPage.goto();
  await loginPage.login('[email protected]', 'gizli123');
  await dashboardPage.expectLoaded();
});

Network Intercepting ve API Mock’lama

Playwright’ın en güçlü özelliklerinden biri HTTP isteklerini yakalayıp manipüle edebilmek. Backend henüz hazır değilse veya belirli hata senaryolarını test etmek istiyorsanız bu özellik hayat kurtarıcı:

// tests/api/network-mock.spec.ts
import { test, expect } from '@playwright/test';

test('API başarısız olduğunda hata mesajı gösterilmeli', async ({ page }) => {
  // /api/products endpoint'ini intercept et
  await page.route('**/api/products', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Sunucu hatası' }),
    });
  });

  await page.goto('/products');
  await expect(page.getByText('Ürünler yüklenemedi')).toBeVisible();
});

test('yavaş network koşullarında loading state gösterilmeli', async ({ page }) => {
  await page.route('**/api/products', async route => {
    // 3 saniye gecikme simüle et
    await page.waitForTimeout(3000);
    await route.continue();
  });

  await page.goto('/products');
  // Loading spinner görünmeli
  await expect(page.getByTestId('loading-spinner')).toBeVisible();
});

test('belirli API çağrısını izle ve doğrula', async ({ page }) => {
  // API isteğini yakala
  const responsePromise = page.waitForResponse('**/api/cart');

  await page.goto('/products');
  await page.getByText('Sepete Ekle').first().click();

  const response = await responsePromise;
  expect(response.status()).toBe(200);

  const body = await response.json();
  expect(body.success).toBe(true);
});

Authentication State Yönetimi

Her test için login yapmak testlerinizi yavaşlatır. Playwright’ın storageState özelliği ile authentication bilgisini bir kez kaydedip sonraki testlerde yeniden kullanabilirsiniz:

// tests/auth/setup.ts - Global setup dosyası
import { chromium, FullConfig } from '@playwright/test';

async function globalSetup(config: FullConfig) {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('http://localhost:3000/login');
  await page.getByLabel('E-posta').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Şifre').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Giriş Yap' }).click();
  await page.waitForURL('**/dashboard');

  // Auth state'i dosyaya kaydet
  await page.context().storageState({ path: 'auth-state.json' });
  await browser.close();
}

export default globalSetup;
# playwright.config.ts içine ekleyin
# globalSetup: './tests/auth/setup.ts',
# use içine ekleyin:
# storageState: 'auth-state.json'

Bu sayede login gerektiren tüm testler mevcut session’ı kullanır, her seferinde login yapmak zorunda kalmaz.

Fixture Kullanımı

Playwright’ın fixture sistemi, test verilerini ve bağlamları merkezi olarak yönetmenizi sağlar. Gerçek projelerde çok işinize yarar:

// tests/fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type TestFixtures = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
  authenticatedPage: void;
};

export const test = base.extend<TestFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  dashboardPage: async ({ page }, use) => {
    const dashboardPage = new DashboardPage(page);
    await use(dashboardPage);
  },

  authenticatedPage: async ({ page, loginPage }, use) => {
    await loginPage.goto();
    await loginPage.login(
      process.env.TEST_USER_EMAIL!,
      process.env.TEST_USER_PASSWORD!
    );
    await use();
  },
});

export { expect };

// Kullanımı
// tests/dashboard/dashboard.spec.ts
import { test, expect } from '../fixtures';

test('authenticated kullanıcı dashboard görür', async ({
  page,
  authenticatedPage,
  dashboardPage,
}) => {
  await dashboardPage.expectLoaded();
  await expect(page.getByTestId('stats-widget')).toBeVisible();
});

CI/CD Entegrasyonu

Playwright’ı GitHub Actions ile entegre etmek oldukça basit. Init scripti zaten bir workflow dosyası oluşturuyor ama production için daha gelişmiş bir versiyon kullanalım:

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Bağımlılıkları yükle
        run: npm ci

      - name: Playwright tarayıcılarını yükle
        run: npx playwright install --with-deps ${{ matrix.browser }}

      - name: Testleri çalıştır
        run: npx playwright test --project=${{ matrix.browser }}
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

      - name: Test raporunu yükle
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ matrix.browser }}
          path: playwright-report/
          retention-days: 30

Matrix strategy ile üç tarayıcıyı paralel çalıştırıyoruz. Bu, test süresini üçe bölüyor.

Testleri Çalıştırma ve Debug

Günlük geliştirme sürecinde kullanacağınız komutlar:

# Tüm testleri çalıştır
npx playwright test

# Belirli bir dosyayı çalıştır
npx playwright test tests/auth/login.spec.ts

# Belirli bir tarayıcıda çalıştır
npx playwright test --project=firefox

# UI modunda çalıştır (görsel, interaktif)
npx playwright test --ui

# Debug modunda çalıştır (her adımda duraklar)
npx playwright test --debug

# Belirli test adıyla filtrele
npx playwright test --grep "başarılı giriş"

# Başarısız testleri tekrar çalıştır
npx playwright test --last-failed

# Test raporunu aç
npx playwright show-report

# Trace dosyasını görüntüle
npx playwright show-trace trace.zip

--ui modu özellikle geliştirme sırasında çok işe yarıyor. Testleri tek tek çalıştırabilir, adım adım ilerleyebilir, DOM’u inceleyebilirsiniz.

Gerçek Dünya Senaryosu: E-ticaret Sepet Testi

Tüm öğrendiklerimizi bir arada kullanan kapsamlı bir senaryo:

// tests/ecommerce/cart.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Alışveriş Sepeti', () => {
  test.beforeEach(async ({ page }) => {
    // Mock ürün listesi
    await page.route('**/api/products', async route => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: 'Laptop', price: 25000, stock: 5 },
          { id: 2, name: 'Mouse', price: 500, stock: 0 },
          { id: 3, name: 'Klavye', price: 1200, stock: 10 },
        ]),
      });
    });

    await page.goto('/products');
  });

  test('ürün sepete eklenmeli', async ({ page }) => {
    await page.getByTestId('product-1').getByText('Sepete Ekle').click();

    await expect(page.getByTestId('cart-badge')).toHaveText('1');
    await expect(page.getByText('Laptop sepete eklendi')).toBeVisible();
  });

  test('stokta olmayan ürün sepete eklenememeli', async ({ page }) => {
    const mouseCard = page.getByTestId('product-2');

    await expect(mouseCard.getByText('Stok Yok')).toBeVisible();
    await expect(mouseCard.getByText('Sepete Ekle')).toBeDisabled();
  });

  test('sepet toplamı doğru hesaplanmalı', async ({ page }) => {
    await page.getByTestId('product-1').getByText('Sepete Ekle').click();
    await page.getByTestId('product-3').getByText('Sepete Ekle').click();

    await page.getByTestId('cart-icon').click();

    // 25000 + 1200 = 26200
    await expect(page.getByTestId('cart-total')).toHaveText('26.200 TL');
  });

  test('mobil görünümde sepet drawer olarak açılmalı', async ({ browser }) => {
    const context = await browser.newContext({
      viewport: { width: 375, height: 812 },
      isMobile: true,
    });
    const page = await context.newPage();

    await page.goto('/products');
    await page.getByTestId('cart-icon').click();

    // Mobilde drawer açılmalı, modal değil
    await expect(page.getByRole('complementary', { name: 'Sepet' })).toBeVisible();
    await expect(page.getByTestId('cart-drawer')).toHaveCSS('position', 'fixed');

    await context.close();
  });
});

Test Raporları ve Monitoring

Playwright HTML reporter son derece bilgilendirici. Her başarısız test için screenshot, video ve trace kaydı sunar. Bunları ekibinizle paylaşmak için Artifact storage veya Playwright Cloud (eski adıyla Playwright Trace Viewer) kullanabilirsiniz.

CI ortamında test sonuçlarını Slack’e bildirmek için şu basit scripti kullanıyorum:

#!/bin/bash
# scripts/notify-slack.sh

RESULT_FILE="test-results/results.xml"
PASSED=$(grep -oP 'tests="K[0-9]+' $RESULT_FILE)
FAILED=$(grep -oP 'failures="K[0-9]+' $RESULT_FILE)

if [ "$FAILED" -gt "0" ]; then
  MESSAGE="Playwright Testleri BAŞARISIZ: $FAILED test hata aldı, $PASSED test geçti."
  COLOR="danger"
else
  MESSAGE="Playwright Testleri BAŞARILI: Tüm $PASSED test geçti."
  COLOR="good"
fi

curl -X POST $SLACK_WEBHOOK_URL 
  -H 'Content-type: application/json' 
  --data "{
    "attachments": [{
      "color": "$COLOR",
      "text": "$MESSAGE"
    }]
  }"

Sonuç

Playwright, modern web uygulamalarının test ihtiyaçlarını karşılamak için tasarlanmış olgun bir araç. Kurulumu kolay, konfigürasyonu esnek ve cross-browser desteği gerçek anlamda güçlü. Özellikle şu noktalara değer veriyorum:

  • Auto-wait mekanizması sayesinde test kodunuz çok daha temiz kalıyor, waitForElement karmaşasından kurtuluyorsunuz
  • Trace viewer ile başarısız testleri debug etmek artık saniyeler alıyor
  • Network mocking özelliği frontend ve backend ekiplerin birbirinden bağımsız test yazmasını mümkün kılıyor
  • Paralel çalışma CI/CD pipeline sürelerinizi ciddi ölçüde kısaltıyor

Eğer projenizde hâlâ manuel test yapıyorsanız veya eski bir test altyapısı kullanıyorsanız, Playwright’a geçişi bir sprint’e sığdırabilirsiniz. İlk hafta temel kurulumu ve birkaç kritik user journey testini yazın, sonrasında kademeli olarak coverage’ınızı artırın. Mükemmel başlangıç aramayın, testlerin var olması hiç olmamasından her zaman iyidir.

Sorularınız olursa yorumlarda buluşalım. Bir sonraki yazıda Playwright ile visual regression testing konusunu ele alacağım.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir