לדלג לתוכן

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

בדיקות E2E עם Playwright

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


מה זה בדיקות E2E

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

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

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

הגדרת Playwright

# התקנה
npm init playwright@latest

# מתקין את Playwright, דפדפנים, ויוצר קבצי הגדרה
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI, // מונע test.only ב-CI
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry', // שומר trace לבדיקות שנכשלו
    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 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('/');

  // בדיקת כותרת הדף
  await expect(page).toHaveTitle(/דף הבית/);

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

  // בדיקת ניווט
  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 - מציאת אלמנטים

// לפי role - הדרך המומלצת
page.getByRole('button', { name: 'שלח' });
page.getByRole('heading', { level: 1 });
page.getByRole('link', { name: 'אודות' });
page.getByRole('textbox', { name: 'אימייל' });
page.getByRole('checkbox', { name: 'אני מסכים' });

// לפי טקסט
page.getByText('ברוכים הבאים');
page.getByText(/שלום/); // regex

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

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

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

// לפי test-id
page.getByTestId('submit-button');

// לפי CSS selector (שימוש אחרון)
page.locator('.product-card');
page.locator('#main-content');

Assertions - בדיקות

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

// בדיקת טקסט
await expect(page.getByRole('heading')).toHaveText('כותרת');
await expect(page.getByRole('heading')).toContainText('כותר');

// בדיקת ערכים
await expect(page.getByLabel('אימייל')).toHaveValue('user@example.com');

// בדיקת URL
await expect(page).toHaveURL('/about');
await expect(page).toHaveURL(/\/products\/.*/);

// בדיקת כותרת דף
await expect(page).toHaveTitle('דף הבית');

// בדיקת כמות אלמנטים
await expect(page.getByRole('listitem')).toHaveCount(5);

// בדיקת CSS
await expect(page.getByRole('button')).toHaveCSS('background-color', 'rgb(59, 130, 246)');

// בדיקת attributes
await expect(page.getByRole('button')).toBeDisabled();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('checkbox')).toBeChecked();

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

לחיצה

// לחיצה רגילה
await page.getByRole('button', { name: 'שלח' }).click();

// לחיצה כפולה
await page.getByText('פריט').dblclick();

// לחיצה ימנית
await page.getByText('פריט').click({ button: 'right' });

מילוי טופס

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

// ניקוי + הקלדה
await page.getByLabel('חיפוש').clear();
await page.getByLabel('חיפוש').fill('React');

// בחירה מ-select
await page.getByLabel('מדינה').selectOption('IL');

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

הקלדה תו-תו

// כשצריך לדמות הקלדה אמיתית (עם אירועים)
await page.getByLabel('חיפוש').pressSequentially('React', { delay: 100 });

// לחיצת מקשים
await page.getByLabel('חיפוש').press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Tab');

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

  • Playwright ממתין אוטומטית לאלמנטים לפני שמבצע פעולות
  • אין צורך ב-sleep או waitFor ברוב המקרים
// Playwright ממתין אוטומטית שהכפתור יהיה גלוי ומופעל
await page.getByRole('button', { name: 'שלח' }).click();

// המתנה מפורשת כשצריך
await page.waitForURL('/dashboard');
await page.waitForResponse('**/api/users');

// המתנה לאלמנט
await page.getByText('נטען בהצלחה').waitFor();
await page.getByText('טוען...').waitFor({ state: 'hidden' });

// המתנה לבקשת רשת
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 }) => {
  // שלב 1: כניסה לחנות
  await page.goto('/');
  await expect(page.getByRole('heading')).toContainText('החנות שלנו');

  // שלב 2: בחירת מוצר
  await page.getByRole('link', { name: 'מוצרים' }).click();
  await expect(page).toHaveURL('/products');

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

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

  // שלב 4: מעבר לסל
  await page.getByRole('link', { name: 'סל קניות' }).click();
  await expect(page).toHaveURL('/cart');
  await expect(page.getByText('אוזניות Sony')).toBeVisible();

  // שלב 5: מעבר לתשלום
  await page.getByRole('button', { name: 'לתשלום' }).click();
  await expect(page).toHaveURL('/checkout');

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

  // שלב 7: ביצוע הזמנה
  await page.getByRole('button', { name: 'בצע הזמנה' }).click();

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

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

// צילום מסך ידני
test('should match homepage screenshot', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png');
});

// צילום מסך של אלמנט ספציפי
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');
});

// השוואה עם tolerance
test('should match with threshold', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01, // עד 1% הבדל
  });
});
# עדכון צילומי reference
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 - קיבוץ בדיקות קשורות
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 }) => {
    // 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;
  }
}

// שימוש בבדיקה
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');
});

הרצת בדיקות

# הרצת כל הבדיקות
npx playwright test

# הרצת בדיקות של קובץ ספציפי
npx playwright test e2e/home.spec.ts

# הרצת בדיקה ספציפית
npx playwright test -g "should login"

# הרצה בדפדפן ספציפי
npx playwright test --project=chromium

# הרצה עם UI
npx playwright test --ui

# הרצה בראש פתוח (לא headless)
npx playwright test --headed

# צפייה בדוח
npx playwright show-report

# דיבוג בדיקה
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