Aller au contenu principal

Tests E2E with Playwright

Setup & Installation

# Install Playwright (recommended for Angular)
npm init playwright@latest

# Install browsers
npx playwright install

Running E2E Tests

# Run all tests
npx playwright test

# Run with UI mode (recommended for development)
npx playwright test --ui

# Run specific test file
npx playwright test e2e/login.spec.ts

# Run on specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

# Debug mode (opens browser inspector)
npx playwright test --debug

# Headed mode (see browser)
npx playwright test --headed

# View HTML report
npx playwright show-report

Configuration

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

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

use: {
baseURL: 'http://localhost:4200',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},

projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },

// Mobile viewports
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 12'] } },
],

// Auau-start dev server
webServer: {
command: 'npm run start',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
}
});

Basic Login Test

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

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

test('should display login form', async ({ page }) => {
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();
await expect(page.locator('[data-testid="password-input"]')).toBeVisible();
await expect(page.locator('[data-testid="submit-button"]')).toBeVisible();
});

test('should login successfully', async ({ page }) => {
await page.fill('[data-testid="email-input"]', 'test@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="submit-button"]');

await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('h1')).toContainText('Dashboard');
});

test('should show validation errors', async ({ page }) => {
await page.click('[data-testid="submit-button"]');

await expect(page.locator('.error-message'))
.toContainText('Email is required');
});

test('should handle wrong credentials', async ({ page }) => {
await page.fill('[data-testid="email-input"]', 'wrong@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpass');
await page.click('[data-testid="submit-button"]');

await expect(page.locator('.error-message'))
.toContainText('Invalid credentials');
});
});

API Mocking

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

test.describe('User List', () => {
test('should load and display users', async ({ page }) => {
// Mock API response
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
])
});
});

await page.goto('/users');

await expect(page.locator('.user-card')).toHaveCount(3);
await expect(page.locator('.user-card').first())
.toContainText('John Doe');
});

test('should handle API error', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
});
});

await page.goto('/users');

await expect(page.locator('.error-message'))
.toContainText('Failed to load users');
});

test('should handle network timeout', async ({ page }) => {
await page.route('**/api/users', async (route) => {
// Simulate slow network
await new Promise(resolve => setTimeout(resolve, 5000));
await route.abort('timedout');
});

await page.goto('/users');

await expect(page.locator('.error-message'))
.toContainText('Request timeout');
});
});

Reusable Fixtures

// e2e/fixtures.ts
import { test as base } from '@playwright/test';

type MyFixtures = {
loginAsUser: (email?: string, password?: string) => Promise<void>;
loginAsAdmin: () => Promise<void>;
};

export const test = base.extend<MyFixtures>({
loginAsUser: async ({ page }, use) => {
const login = async (
email = 'test@example.com',
password = 'password123'
) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', email);
await page.fill('[data-testid="password-input"]', password);
await page.click('[data-testid="submit-button"]');
await page.waitForURL('**/dashboard');
};
await use(login);
},

loginAsAdmin: async ({ page }, use) => {
const login = async () => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'admin@example.com');
await page.fill('[data-testid="password-input"]', 'admin123');
await page.click('[data-testid="submit-button"]');
await page.waitForURL('**/dashboard');
};
await use(login);
}
});

export { expect } from '@playwright/test';

Complete CRUD Flow

// e2e/user-crud.spec.ts
import { test, expect } from './fixtures';

test.describe('User CRUD Operations', () => {
test.beforeEach(async ({ page, loginAsAdmin }) => {
await loginAsAdmin();
await page.goto('/users');
});

test('should create new user', async ({ page }) => {
await page.click('[data-testid="create-user-button"]');
await expect(page).toHaveURL(/.*users\/create/);

await page.fill('[data-testid="name-input"]', 'Alice Cooper');
await page.fill('[data-testid="email-input"]', 'alice@example.com');
await page.selectOption('[data-testid="role-select"]', 'user');
await page.click('[data-testid="submit-button"]');

await expect(page).toHaveURL(/.*users$/);
await expect(page.locator('.success-message'))
.toContainText('User created successfully');
await expect(page.locator('.user-card', { hasText: 'Alice Cooper' }))
.toBeVisible();
});

test('should edit existing user', async ({ page }) => {
const userCard = page.locator('.user-card', { hasText: 'John Doe' });
await userCard.locator('[data-testid="edit-button"]').click();

await page.fill('[data-testid="name-input"]', 'John Updated');
await page.click('[data-testid="submit-button"]');

await expect(page.locator('.user-card', { hasText: 'John Updated' }))
.toBeVisible();
await expect(page.locator('.user-card', { hasText: 'John Doe' }))
.not.toBeVisible();
});

test('should delete user', async ({ page }) => {
const userCard = page.locator('.user-card', { hasText: 'Bob Johnson' });
await userCard.locator('[data-testid="delete-button"]').click();

// Confirm deletion
await page.click('[data-testid="confirm-delete"]');

await expect(page.locator('.success-message'))
.toContainText('User deleted');
await expect(page.locator('.user-card', { hasText: 'Bob Johnson' }))
.not.toBeVisible();
});

test('should search users', async ({ page }) => {
await page.fill('[data-testid="search-input"]', 'john');

await expect(page.locator('.user-card', { hasText: 'John' }))
.toBeVisible();
await expect(page.locator('.user-card', { hasText: 'Jane' }))
.not.toBeVisible();
});
});

Responsive & Mobile Tests

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

test.describe('Responsive Design', () => {
test('mobile view - should show hamburger menu', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');

await expect(page.locator('[data-testid="hamburger-menu"]'))
.toBeVisible();
await expect(page.locator('[data-testid="desktop-nav"]'))
.not.toBeVisible();
});

test('tablet view - should show compact nav', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto('/');

await expect(page.locator('[data-testid="tablet-nav"]'))
.toBeVisible();
});

test('desktop view - should show full navigation', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/');

await expect(page.locator('[data-testid="desktop-nav"]'))
.toBeVisible();
await expect(page.locator('[data-testid="hamburger-menu"]'))
.not.toBeVisible();
});

test('should work on iPhone 12', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');

const title = page.locator('h1');
await expect(title).toBeVisible();

// Check font size is appropriate for mobile
const fontSize = await title.evaluate(el =>
window.getComputedStyle(el).fontSize
);
expect(parseInt(fontSize)).toBeLessThan(32);
});
});

CI/CD Integration

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

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

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
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

- name: Build Angular app
run: npm run build

- name: Run E2E tests
run: npx playwright test

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

- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: test-results/

Avancé Patterns

Page Object Model

// e2e/pages/login.page.ts
import { Page, Locator } 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.locator('[data-testid="email-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.submitButton = page.locator('[data-testid="submit-button"]');
this.errorMessage = page.locator('.error-message');
}

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 getErrorText() {
return await this.errorMessage.textContent();
}
}

// Usage
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';

test('should login with page object', async ({ page }) => {
const loginPage = new LoginPage(page);

await loginPage.goto();
await loginPage.login('test@example.com', 'password123');

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

Visual Regression Tests

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

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

// Take screenshot and compsont
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100
});
});

test('should match button states', async ({ page }) => {
await page.goto('/buttons');

const button = page.locator('[data-testid="primary-button"]');

// Normal state
await expect(button).toHaveScreenshot('button-normal.png');

// Hover state
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');

// Disabled state
await button.evaluate(el => el.setAttribute('disabled', ''));
await expect(button).toHaveScreenshot('button-disabled.png');
});

Bonnes Pratiques

  • Utiliser data-testid attributes for stable selecaurs
  • Mock API calls for predictable tests
  • Test user flows, not implementation details
  • Garder tests independent (can run in any order)
  • Utiliser fixtures for reusable test setup
  • Test critical paths thoroughly
  • Run tests in CI/CD pipeline
  • Test multiple browsers (Chromium, Firefox, WebKit)
  • Test mobile viewports for responsive design
  • Take screenshots on failure (automatique in Playwright)
  • Utiliser Page Object pattern for complex pages
  • Avoid brittle selecaurs (avoid CSS classes, use data-testid)
  • Clean up test data after tests when testing real APIs
  • Garder tests fast (< 30 seconds per test)
  • Utiliser parallel execution for faster test runs
  • Enable trace for debugging failed tests

Debugging Conseils

# Debug mode - opens inspector
npx playwright test --debug

# Debug specific test
npx playwright test e2e/login.spec.ts --debug

# Run in headed mode to see browser
npx playwright test --headed

# Run with UI mode (best for development)
npx playwright test --ui

# Show trace viewer for failed tests
npx playwright show-trace trace.zip

Common Assertions

// Visibility
await expect(page.locator('#element')).toBeVisible();
await expect(page.locator('#element')).not.toBeVisible();
await expect(page.locator('#element')).toBeHidden();

// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('h1')).toContainText('Wel');

// Attributes
await expect(page.locator('button')).toBeDisabled();
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('input')).toHaveValue('test');
await expect(page.locator('a')).toHaveAttribute('href', '/home');

// Count
await expect(page.locator('.user-card')).toHaveCount(5);

// URL
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).auHaveURL('http://localhost:4200/dashboard');

// Screenshot comparison
await expect(page).toHaveScreenshot('page.png');