לדלג לתוכן

10.8 - בדיקות E2E - הרצאה

בדיקות E2E עם Playwright

בשיעור זה נלמד כיצד לכתוב בדיקות מקצה לקצה (End-to-End) עם Playwright - כלי שמדמה את השימוש באפליקציה בדפדפן אמיתי.


מה זה בדיקות E2E

  • בדיקות E2E בודקות את האפליקציה כמו שמשתמש אמיתי היה משתמש בה
  • רצות בדפדפן אמיתי (Chromium, Firefox, WebKit)
  • בודקות תהליכים מלאים: התחברות, מילוי טופס, ניווט בין דפים
  • האיטיות מבין סוגי הבדיקות, אבל מספקות את הביטחון הגבוה ביותר

מתי להשתמש ב-E2E

  • תהליכים קריטיים (הרשמה, התחברות, רכישה)
  • ניווט בין דפים
  • אינטגרציה עם שירותים חיצוניים
  • בדיקת responsive ו-cross-browser
  • בדיקות regression

הגדרת Playwright

# Installation
npm init playwright@latest

# Installs Playwright, browsers, and creates config files
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI, // prevents test.only in CI
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry', // keeps trace for failed tests
    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'] },
    },
  ],

  // Run the dev server before the tests
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

כתיבת בדיקות בסיסיות

ניווט ובדיקת תוכן

// e2e/home.spec.ts
import { test, expect } from '@playwright/test';

test('should display homepage', async ({ page }) => {
  await page.goto('/');

  // Check page title
  await expect(page).toHaveTitle(/דף הבית/);

  // Check main heading
  await expect(page.getByRole('heading', { level: 1 })).toHaveText('ברוכים הבאים');

  // Check navigation
  await expect(page.getByRole('navigation')).toBeVisible();
});

test('should navigate to about page', async ({ page }) => {
  await page.goto('/');

  await page.getByRole('link', { name: 'אודות' }).click();

  await expect(page).toHaveURL('/about');
  await expect(page.getByRole('heading', { level: 1 })).toHaveText('אודות');
});

Locators - מציאת אלמנטים

// By role - the recommended way
page.getByRole('button', { name: 'שלח' });
page.getByRole('heading', { level: 1 });
page.getByRole('link', { name: 'אודות' });
page.getByRole('textbox', { name: 'אימייל' });
page.getByRole('checkbox', { name: 'אני מסכים' });

// By text
page.getByText('ברוכים הבאים');
page.getByText(/שלום/); // regex

// By label
page.getByLabel('אימייל');
page.getByLabel('סיסמה');

// By placeholder
page.getByPlaceholder('הקלידו כאן...');

// By alt text
page.getByAltText('לוגו');

// By test-id
page.getByTestId('submit-button');

// By CSS selector (last resort)
page.locator('.product-card');
page.locator('#main-content');

Assertions - בדיקות

// Check visibility
await expect(page.getByText('שלום')).toBeVisible();
await expect(page.getByText('הודעת שגיאה')).not.toBeVisible();
await expect(page.getByText('שלום')).toBeHidden();

// Check text
await expect(page.getByRole('heading')).toHaveText('כותרת');
await expect(page.getByRole('heading')).toContainText('כותר');

// Check values
await expect(page.getByLabel('אימייל')).toHaveValue('user@example.com');

// Check URL
await expect(page).toHaveURL('/about');
await expect(page).toHaveURL(/\/products\/.*/);

// Check page title
await expect(page).toHaveTitle('דף הבית');

// Check element count
await expect(page.getByRole('listitem')).toHaveCount(5);

// Check CSS
await expect(page.getByRole('button')).toHaveCSS('background-color', 'rgb(59, 130, 246)');

// Check attributes
await expect(page.getByRole('button')).toBeDisabled();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('checkbox')).toBeChecked();

אינטראקציה עם אלמנטים

לחיצה

// Regular click
await page.getByRole('button', { name: 'שלח' }).click();

// Double click
await page.getByText('פריט').dblclick();

// Right click
await page.getByText('פריט').click({ button: 'right' });

מילוי טופס

// Typing
await page.getByLabel('שם').fill('דני כהן');
await page.getByLabel('אימייל').fill('dani@example.com');

// Clear + type
await page.getByLabel('חיפוש').clear();
await page.getByLabel('חיפוש').fill('React');

// Selecting from a select
await page.getByLabel('מדינה').selectOption('IL');

// checkbox
await page.getByRole('checkbox', { name: 'אני מסכים' }).check();
await page.getByRole('checkbox', { name: 'אני מסכים' }).uncheck();

הקלדה תו-תו

// When you need to simulate real typing (with events)
await page.getByLabel('חיפוש').pressSequentially('React', { delay: 100 });

// Key presses
await page.getByLabel('חיפוש').press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Tab');

אסטרטגיות המתנה - Waiting Strategies

  • Playwright ממתין אוטומטית לאלמנטים לפני שמבצע פעולות
  • אין צורך ב-sleep או waitFor ברוב המקרים
// Playwright automatically waits for the button to be visible and enabled
await page.getByRole('button', { name: 'שלח' }).click();

// Explicit wait when needed
await page.waitForURL('/dashboard');
await page.waitForResponse('**/api/users');

// Waiting for an element
await page.getByText('נטען בהצלחה').waitFor();
await page.getByText('טוען...').waitFor({ state: 'hidden' });

// Waiting for a network request
const responsePromise = page.waitForResponse('**/api/products');
await page.getByRole('button', { name: 'טען מוצרים' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);

בדיקת ניווט ותהליכים מרובי דפים

// e2e/shopping.spec.ts
import { test, expect } from '@playwright/test';

test('should complete purchase flow', async ({ page }) => {
  // Step 1: enter the store
  await page.goto('/');
  await expect(page.getByRole('heading')).toContainText('החנות שלנו');

  // Step 2: select a product
  await page.getByRole('link', { name: 'מוצרים' }).click();
  await expect(page).toHaveURL('/products');

  await page.getByText('אוזניות Sony').click();
  await expect(page).toHaveURL(/\/products\/.*/);

  // Step 3: add to cart
  await page.getByRole('button', { name: 'הוסף לסל' }).click();
  await expect(page.getByText('המוצר נוסף לסל')).toBeVisible();

  // Step 4: go to the cart
  await page.getByRole('link', { name: 'סל קניות' }).click();
  await expect(page).toHaveURL('/cart');
  await expect(page.getByText('אוזניות Sony')).toBeVisible();

  // Step 5: go to checkout
  await page.getByRole('button', { name: 'לתשלום' }).click();
  await expect(page).toHaveURL('/checkout');

  // Step 6: fill in details
  await page.getByLabel('שם מלא').fill('דני כהן');
  await page.getByLabel('אימייל').fill('dani@example.com');
  await page.getByLabel('כתובת').fill('הרצל 1, תל אביב');

  // Step 7: place the order
  await page.getByRole('button', { name: 'בצע הזמנה' }).click();

  // Step 8: confirmation
  await expect(page).toHaveURL(/\/order\/.*/);
  await expect(page.getByText('ההזמנה בוצעה בהצלחה')).toBeVisible();
});

צילומי מסך ו-Visual Regression

// Manual screenshot
test('should match homepage screenshot', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png');
});

// Screenshot of a specific element
test('should match product card screenshot', async ({ page }) => {
  await page.goto('/products');
  const card = page.getByTestId('product-card-1');
  await expect(card).toHaveScreenshot('product-card.png');
});

// Comparison with tolerance
test('should match with threshold', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01, // up to 1% difference
  });
});
# Update reference screenshots
npx playwright test --update-snapshots

הרצה ב-CI

# .github/workflows/e2e.yml
name: E2E Tests

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

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm ci

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

      - name: Build app
        run: npm run build

      - name: Run E2E tests
        run: npx playwright test

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

טיפים ו-Best Practices

ארגון בדיקות

// e2e/auth.spec.ts - grouping related tests
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('should login with valid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('אימייל').fill('user@example.com');
    await page.getByLabel('סיסמה').fill('password123');
    await page.getByRole('button', { name: 'התחבר' }).click();

    await expect(page).toHaveURL('/dashboard');
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('אימייל').fill('user@example.com');
    await page.getByLabel('סיסמה').fill('wrong');
    await page.getByRole('button', { name: 'התחבר' }).click();

    await expect(page.getByText('אימייל או סיסמה שגויים')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('should logout', async ({ page }) => {
    // initial login
    await page.goto('/login');
    await page.getByLabel('אימייל').fill('user@example.com');
    await page.getByLabel('סיסמה').fill('password123');
    await page.getByRole('button', { name: 'התחבר' }).click();
    await expect(page).toHaveURL('/dashboard');

    // logout
    await page.getByRole('button', { name: 'התנתק' }).click();
    await expect(page).toHaveURL('/');
  });
});

Page Object Model - תבנית עיצוב

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

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

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('אימייל');
    this.passwordInput = page.getByLabel('סיסמה');
    this.submitButton = page.getByRole('button', { name: 'התחבר' });
    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 getError() {
    return this.errorMessage;
  }
}

// Usage in a test
test('should login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password123');
  await expect(page).toHaveURL('/dashboard');
});

הרצת בדיקות

# Run all tests
npx playwright test

# Run tests from a specific file
npx playwright test e2e/home.spec.ts

# Run a specific test
npx playwright test -g "should login"

# Run in a specific browser
npx playwright test --project=chromium

# Run with UI
npx playwright test --ui

# Run headed (not headless)
npx playwright test --headed

# View the report
npx playwright show-report

# Debug a test
npx playwright test --debug

סיכום

  • בדיקות E2E בודקות את האפליקציה כמו שמשתמש אמיתי היה משתמש
  • Playwright תומך ב-Chromium, Firefox ו-WebKit
  • Locators: getByRole (מומלץ), getByText, getByLabel, getByTestId
  • אינטראקציות: click, fill, check, selectOption, press
  • Playwright ממתין אוטומטית לאלמנטים - אין צורך ב-sleep
  • Visual regression עם toHaveScreenshot
  • Page Object Model מפשט תחזוקה של בדיקות
  • הרצה ב-CI עם GitHub Actions