Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik12 min czytania

Playwright

Playwright to framework do automatyzacji przeglądarek i testów E2E od Microsoft. Kompletny przewodnik - selektory, asercje, visual testing, API testing i CI/CD.

Playwright - Kompletny Przewodnik po Testach E2E

Czym jest Playwright?

Playwright to nowoczesny framework do automatyzacji przeglądarek i testów end-to-end stworzony przez Microsoft. Wspiera wszystkie główne przeglądarki (Chromium, Firefox, WebKit) z jednym spójnym API, oferując szybkie, niezawodne i cross-browser testowanie aplikacji webowych.

W przeciwieństwie do starszych narzędzi jak Selenium, Playwright został zaprojektowany od podstaw z myślą o nowoczesnych aplikacjach - obsługuje auto-waiting, web components, shadow DOM, iframes i wiele innych trudnych scenariuszy bez dodatkowej konfiguracji.

Dlaczego Playwright?

Kluczowe zalety

  1. Cross-browser - Chromium, Firefox, WebKit z jednym API
  2. Auto-waiting - Automatyczne czekanie na elementy
  3. Szybkość - Równoległe wykonywanie testów
  4. Niezawodność - Brak flaky tests dzięki smart retries
  5. Codegen - Nagrywanie testów przez interakcję
  6. Trace Viewer - Debugowanie z pełnym kontekstem
  7. API Testing - Testy REST API wbudowane

Playwright vs Cypress vs Selenium

CechaPlaywrightCypressSelenium
PrzeglądarkiChromium, Firefox, WebKitChromium, Firefox (beta)Wszystkie
SzybkośćBardzo szybkiSzybkiWolny
Auto-waitingWbudowaneWbudowaneRęczne
ParallelNatywnePłatne (Cloud)Wymaga Grid
iframesPełne wsparcieOgraniczonePełne
Mobile emulationTakNiePrzez Appium
API testingWbudowanePluginZewnętrzne
JęzykJS/TS, Python, .NET, JavaTylko JS/TSWszystkie

Instalacja i konfiguracja

Nowy projekt

Code
Bash
# Inicjalizacja projektu Playwright
npm init playwright@latest

# Odpowiedz na pytania:
# - TypeScript lub JavaScript
# - Folder na testy (tests/)
# - Dodać GitHub Actions workflow?
# - Zainstalować przeglądarki?

Struktura projektu

Code
TEXT
my-project/
├── tests/
│   ├── example.spec.ts
│   └── auth.spec.ts
├── playwright.config.ts
├── package.json
└── .github/
    └── workflows/
        └── playwright.yml

Konfiguracja playwright.config.ts

Code
TypeScript
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  // Folder z testami
  testDir: './tests',

  // Uruchom testy równolegle
  fullyParallel: true,

  // Fail build on CI if you accidentally left test.only
  forbidOnly: !!process.env.CI,

  // Retry failed tests on CI
  retries: process.env.CI ? 2 : 0,

  // Workers - równoległe procesy
  workers: process.env.CI ? 1 : undefined,

  // Reporter
  reporter: [
    ['html'],
    ['list'],
    process.env.CI ? ['github'] : ['line']
  ],

  // Globalne ustawienia testów
  use: {
    // Base URL
    baseURL: 'http://localhost:3000',

    // Trace on failure
    trace: 'on-first-retry',

    // Screenshot on failure
    screenshot: 'only-on-failure',

    // Video on failure
    video: 'on-first-retry',
  },

  // Projekty = konfiguracje przeglądarek
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // Mobile
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  // Uruchom dev server przed testami
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Pisanie testów

Podstawowa struktura

Code
TypeScript
import { test, expect } from '@playwright/test'

test.describe('Homepage', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/')
  })

  test('has correct title', async ({ page }) => {
    await expect(page).toHaveTitle(/My App/)
  })

  test('navigation works', async ({ page }) => {
    await page.getByRole('link', { name: 'About' }).click()
    await expect(page).toHaveURL('/about')
  })
})

Locators - znajdowanie elementów

Code
TypeScript
import { test, expect } from '@playwright/test'

test('locators examples', async ({ page }) => {
  await page.goto('/form')

  // ✅ Rekomendowane - semantyczne locatory
  // Role-based (accessibility)
  await page.getByRole('button', { name: 'Submit' }).click()
  await page.getByRole('link', { name: 'Home' }).click()
  await page.getByRole('heading', { name: 'Welcome' })
  await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com')
  await page.getByRole('checkbox', { name: 'Accept terms' }).check()

  // Label-based
  await page.getByLabel('Email').fill('test@example.com')
  await page.getByLabel('Password').fill('secret123')

  // Placeholder
  await page.getByPlaceholder('Enter your email').fill('test@example.com')

  // Text content
  await page.getByText('Welcome back')
  await page.getByText(/welcome/i) // Regex, case-insensitive

  // Test ID (dla skomplikowanych przypadków)
  await page.getByTestId('submit-button').click()

  // ⚠️ Fallback - CSS/XPath (mniej stabilne)
  await page.locator('.submit-btn').click()
  await page.locator('#email-input').fill('test@example.com')
  await page.locator('button[type="submit"]').click()
  await page.locator('//button[text()="Submit"]').click() // XPath
})

Łańcuchowanie locatorów

Code
TypeScript
test('chained locators', async ({ page }) => {
  // Znajdź element w kontekście innego
  const form = page.locator('form#login')
  await form.getByLabel('Email').fill('test@example.com')
  await form.getByLabel('Password').fill('secret')
  await form.getByRole('button', { name: 'Login' }).click()

  // Filtrowanie
  const rows = page.getByRole('row')
  const activeRow = rows.filter({ hasText: 'Active' })
  await activeRow.getByRole('button', { name: 'Edit' }).click()

  // Nth element
  await page.getByRole('listitem').nth(2).click() // Trzeci element
  await page.getByRole('listitem').first().click()
  await page.getByRole('listitem').last().click()
})

Asercje

Code
TypeScript
import { test, expect } from '@playwright/test'

test('assertions examples', async ({ page }) => {
  await page.goto('/dashboard')

  // Page assertions
  await expect(page).toHaveTitle('Dashboard')
  await expect(page).toHaveURL(/dashboard/)
  await expect(page).toHaveURL('http://localhost:3000/dashboard')

  // Element visibility
  await expect(page.getByText('Welcome')).toBeVisible()
  await expect(page.getByText('Error')).toBeHidden()
  await expect(page.getByTestId('loading')).not.toBeVisible()

  // Element state
  await expect(page.getByRole('button')).toBeEnabled()
  await expect(page.getByRole('button')).toBeDisabled()
  await expect(page.getByRole('checkbox')).toBeChecked()
  await expect(page.getByRole('textbox')).toBeEditable()
  await expect(page.getByRole('textbox')).toBeFocused()

  // Element content
  await expect(page.getByRole('heading')).toHaveText('Dashboard')
  await expect(page.getByRole('heading')).toContainText('Dash')
  await expect(page.getByRole('textbox')).toHaveValue('initial value')
  await expect(page.getByRole('textbox')).toBeEmpty()

  // Element attributes
  await expect(page.getByRole('link')).toHaveAttribute('href', '/about')
  await expect(page.getByRole('img')).toHaveAttribute('alt', /logo/i)

  // CSS
  await expect(page.getByTestId('box')).toHaveCSS('color', 'rgb(0, 0, 0)')
  await expect(page.getByRole('button')).toHaveClass(/primary/)

  // Count
  await expect(page.getByRole('listitem')).toHaveCount(5)

  // Soft assertions (nie przerywają testu)
  await expect.soft(page.getByText('Optional')).toBeVisible()
})

Interakcje

Code
TypeScript
test('interactions', async ({ page }) => {
  await page.goto('/form')

  // Klikanie
  await page.getByRole('button').click()
  await page.getByRole('button').click({ button: 'right' }) // Right click
  await page.getByRole('button').dblclick() // Double click
  await page.getByRole('button').click({ modifiers: ['Control'] }) // Ctrl+click

  // Hover
  await page.getByText('Menu').hover()

  // Wypełnianie formularzy
  await page.getByLabel('Email').fill('test@example.com')
  await page.getByLabel('Email').clear()
  await page.getByLabel('Email').type('test@example.com') // Keystroke by keystroke

  // Press keys
  await page.getByLabel('Search').press('Enter')
  await page.keyboard.press('Escape')
  await page.keyboard.press('Control+A')

  // Select dropdown
  await page.getByLabel('Country').selectOption('poland')
  await page.getByLabel('Country').selectOption({ label: 'Poland' })
  await page.getByLabel('Multi').selectOption(['opt1', 'opt2'])

  // Checkbox & radio
  await page.getByLabel('Accept').check()
  await page.getByLabel('Accept').uncheck()
  await page.getByLabel('Accept').setChecked(true)

  // File upload
  await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
  await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf'])

  // Drag and drop
  await page.getByTestId('source').dragTo(page.getByTestId('target'))

  // Focus
  await page.getByLabel('Email').focus()
  await page.getByLabel('Email').blur()
})

Czekanie i timeouty

Code
TypeScript
test('waiting', async ({ page }) => {
  await page.goto('/dashboard')

  // Auto-waiting - Playwright automatycznie czeka na elementy
  // Poniższe samo w sobie czeka aż element będzie widoczny i klikalny
  await page.getByRole('button').click()

  // Explicit waits
  await page.waitForSelector('.loaded')
  await page.waitForLoadState('networkidle')
  await page.waitForLoadState('domcontentloaded')
  await page.waitForURL('/dashboard')

  // Wait for response
  const responsePromise = page.waitForResponse('**/api/data')
  await page.getByRole('button', { name: 'Load' }).click()
  const response = await responsePromise

  // Wait for request
  const requestPromise = page.waitForRequest('**/api/submit')
  await page.getByRole('button', { name: 'Submit' }).click()
  await requestPromise

  // Custom wait
  await page.waitForFunction(() => {
    return document.querySelector('.counter')?.textContent === '10'
  })

  // Timeout override
  await page.getByRole('button').click({ timeout: 10000 })

  // Poll until condition
  await expect(async () => {
    const count = await page.getByTestId('count').textContent()
    expect(parseInt(count!)).toBeGreaterThan(5)
  }).toPass({ timeout: 10000 })
})

Zaawansowane scenariusze

Autentykacja

TStests/auth.setup.ts
TypeScript
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test'

const authFile = 'playwright/.auth/user.json'

setup('authenticate', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('user@example.com')
  await page.getByLabel('Password').fill('password123')
  await page.getByRole('button', { name: 'Sign in' }).click()

  // Wait for redirect after login
  await page.waitForURL('/dashboard')

  // Save authentication state
  await page.context().storageState({ path: authFile })
})
TSplaywright.config.ts
TypeScript
// playwright.config.ts
export default defineConfig({
  projects: [
    // Setup project
    { name: 'setup', testMatch: /.*\.setup\.ts/ },

    // Tests that need auth
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
})
TStests/dashboard.spec.ts
TypeScript
// tests/dashboard.spec.ts
import { test, expect } from '@playwright/test'

// Ten test używa zalogowanego stanu
test('dashboard shows user data', async ({ page }) => {
  await page.goto('/dashboard')
  await expect(page.getByText('Welcome, User')).toBeVisible()
})

Multiple contexts (multi-user)

Code
TypeScript
test('admin and user interaction', async ({ browser }) => {
  // Admin context
  const adminContext = await browser.newContext({
    storageState: 'playwright/.auth/admin.json'
  })
  const adminPage = await adminContext.newPage()

  // User context
  const userContext = await browser.newContext({
    storageState: 'playwright/.auth/user.json'
  })
  const userPage = await userContext.newPage()

  // Admin creates something
  await adminPage.goto('/admin/posts')
  await adminPage.getByRole('button', { name: 'New Post' }).click()
  await adminPage.getByLabel('Title').fill('Test Post')
  await adminPage.getByRole('button', { name: 'Publish' }).click()

  // User sees it
  await userPage.goto('/posts')
  await expect(userPage.getByText('Test Post')).toBeVisible()

  // Cleanup
  await adminContext.close()
  await userContext.close()
})

API Testing

Code
TypeScript
import { test, expect } from '@playwright/test'

test.describe('API Tests', () => {
  test('GET /api/users', async ({ request }) => {
    const response = await request.get('/api/users')

    expect(response.ok()).toBeTruthy()
    expect(response.status()).toBe(200)

    const users = await response.json()
    expect(users).toHaveLength(10)
    expect(users[0]).toHaveProperty('email')
  })

  test('POST /api/users', async ({ request }) => {
    const response = await request.post('/api/users', {
      data: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    })

    expect(response.status()).toBe(201)

    const user = await response.json()
    expect(user.name).toBe('John Doe')
  })

  test('with authentication', async ({ request }) => {
    const response = await request.get('/api/me', {
      headers: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`
      }
    })

    expect(response.ok()).toBeTruthy()
  })
})

Visual Regression Testing

Code
TypeScript
import { test, expect } from '@playwright/test'

test('visual regression', async ({ page }) => {
  await page.goto('/dashboard')

  // Full page screenshot
  await expect(page).toHaveScreenshot('dashboard.png')

  // Element screenshot
  const chart = page.getByTestId('revenue-chart')
  await expect(chart).toHaveScreenshot('revenue-chart.png')

  // With options
  await expect(page).toHaveScreenshot('dashboard-full.png', {
    fullPage: true,
    maxDiffPixels: 100,
    threshold: 0.2,
  })
})
Code
Bash
# Aktualizacja baseline screenshots
npx playwright test --update-snapshots

Mock i intercept

Code
TypeScript
test('mock API responses', async ({ page }) => {
  // Mock konkretnego endpointu
  await page.route('**/api/users', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'Mock User' }
      ])
    })
  })

  await page.goto('/users')
  await expect(page.getByText('Mock User')).toBeVisible()
})

test('modify response', async ({ page }) => {
  await page.route('**/api/products', async route => {
    const response = await route.fetch()
    const json = await response.json()

    // Modify response
    json.products = json.products.slice(0, 3)

    await route.fulfill({ response, json })
  })

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

test('abort requests', async ({ page }) => {
  // Block images
  await page.route('**/*.{png,jpg,jpeg}', route => route.abort())

  // Block analytics
  await page.route('**/analytics/**', route => route.abort())

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

test('delay response', async ({ page }) => {
  await page.route('**/api/slow', async route => {
    await new Promise(resolve => setTimeout(resolve, 3000))
    await route.continue()
  })

  await page.goto('/page')
  // Test loading state
  await expect(page.getByText('Loading...')).toBeVisible()
})

Fixtures (Custom setup)

TSfixtures.ts
TypeScript
// fixtures.ts
import { test as base, expect } from '@playwright/test'

// Extend base test with custom fixtures
export const test = base.extend<{
  adminPage: Page
  userPage: Page
  todoApp: TodoApp
}>({
  // Admin page fixture
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/admin.json'
    })
    const page = await context.newPage()
    await use(page)
    await context.close()
  },

  // User page fixture
  userPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/user.json'
    })
    const page = await context.newPage()
    await use(page)
    await context.close()
  },

  // Page Object fixture
  todoApp: async ({ page }, use) => {
    const todoApp = new TodoApp(page)
    await todoApp.goto()
    await use(todoApp)
  },
})

export { expect }

// Page Object Model
class TodoApp {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/todos')
  }

  async addTodo(text: string) {
    await this.page.getByPlaceholder('What needs to be done?').fill(text)
    await this.page.keyboard.press('Enter')
  }

  async toggleTodo(text: string) {
    await this.page.getByRole('listitem')
      .filter({ hasText: text })
      .getByRole('checkbox')
      .click()
  }

  async deleteTodo(text: string) {
    await this.page.getByRole('listitem')
      .filter({ hasText: text })
      .hover()
    await this.page.getByRole('button', { name: 'Delete' }).click()
  }

  async getTodoCount() {
    return await this.page.getByRole('listitem').count()
  }
}
TStests/todo.spec.ts
TypeScript
// tests/todo.spec.ts
import { test, expect } from '../fixtures'

test('can add and complete todos', async ({ todoApp }) => {
  await todoApp.addTodo('Buy groceries')
  await todoApp.addTodo('Do laundry')

  expect(await todoApp.getTodoCount()).toBe(2)

  await todoApp.toggleTodo('Buy groceries')
  await todoApp.deleteTodo('Do laundry')

  expect(await todoApp.getTodoCount()).toBe(1)
})

Uruchamianie testów

Podstawowe komendy

Code
Bash
# Uruchom wszystkie testy
npx playwright test

# Konkretny plik
npx playwright test tests/login.spec.ts

# Konkretny test (grep)
npx playwright test -g "login works"

# Konkretna przeglądarka
npx playwright test --project=chromium
npx playwright test --project=firefox

# Headed mode (widoczna przeglądarka)
npx playwright test --headed

# Debug mode
npx playwright test --debug

# UI mode (interaktywny)
npx playwright test --ui

# Tylko failed testy
npx playwright test --last-failed

# Powtórz test X razy
npx playwright test --repeat-each=3

Raporty

Code
Bash
# HTML report
npx playwright show-report

# Trace viewer
npx playwright show-trace trace.zip

Codegen - nagrywanie testów

Code
Bash
# Nagraj interakcje i wygeneruj kod
npx playwright codegen https://example.com

# Z emulacją urządzenia
npx playwright codegen --device="iPhone 13"

# Z viewport
npx playwright codegen --viewport-size=800,600

# Zapisz do pliku
npx playwright codegen -o tests/recorded.spec.ts

CI/CD Integration

GitHub Actions

.github/workflows/playwright.yml
YAML
# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

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

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Run Playwright tests
        run: npx playwright test

      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Sharding (równoległe CI)

Code
YAML
jobs:
  test:
    strategy:
      matrix:
        shard: [1/4, 2/4, 3/4, 4/4]

    steps:
      - name: Run tests
        run: npx playwright test --shard=${{ matrix.shard }}

Best Practices

Stabilne selektory

Code
TypeScript
// ✅ Dobre - semantyczne, stabilne
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email')
page.getByTestId('login-form')

// ❌ Unikaj - kruche, zależne od implementacji
page.locator('.btn-primary')
page.locator('#submit-btn')
page.locator('div > button:nth-child(2)')

Izolacja testów

Code
TypeScript
// ✅ Każdy test powinien być niezależny
test.beforeEach(async ({ page }) => {
  // Reset state przed każdym testem
  await page.goto('/')
})

// ❌ Unikaj zależności między testami
// Test A nie powinien polegać na stanie z Test B

Test ID dla trudnych elementów

Code
HTML
<!-- W aplikacji -->
<button data-testid="checkout-button">Complete Purchase</button>
Code
TypeScript
// W teście
await page.getByTestId('checkout-button').click()

FAQ - Najczęściej zadawane pytania

Jak debugować failed testy?

  1. Uruchom z --debug: npx playwright test --debug
  2. Użyj Trace Viewer do analizy trace.zip
  3. Dodaj await page.pause() w kodzie testu
  4. Użyj UI mode: npx playwright test --ui

Jak testować responsywność?

Code
TypeScript
test('mobile layout', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 })
  await page.goto('/')
  await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible()
})

Jak obsłużyć popup/dialog?

Code
TypeScript
page.on('dialog', dialog => dialog.accept())
await page.getByRole('button', { name: 'Delete' }).click()

Jak testować nowe okno/tab?

Code
TypeScript
const [newPage] = await Promise.all([
  page.waitForEvent('popup'),
  page.getByRole('link', { name: 'Open in new tab' }).click()
])
await expect(newPage).toHaveURL('/new-page')

Podsumowanie

Playwright to potężne narzędzie do testowania E2E oferujące:

  • Cross-browser - Jeden test, wszystkie przeglądarki
  • Auto-waiting - Koniec z flaky tests
  • Szybkość - Równoległe wykonywanie
  • Debugging - Trace viewer, UI mode, codegen
  • Elastyczność - API testing, visual testing, mocking

Idealne dla zespołów chcących niezawodnych, szybkich testów E2E.