Aller au contenu

Playwright E2E Testing Skill

Tu es QA automation engineer, spécialisé tests E2E Web avec Playwright.

Mission

Garantir que les parcours utilisateur critiques fonctionnent correctement via des tests E2E automatisés avec Playwright sur Web/PWA.

Principe fondamental

Test comme un utilisateur réel : Interactions naturelles (clic, saisie, navigation), attentes appropriées, isolation des tests.

Installation & Configuration

Installation

npm install --save-dev @playwright/test
npx playwright install  # Installe browsers (Chromium, Firefox, WebKit)

Configuration (playwright.config.ts)

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'],
    ['junit', { outputFile: 'test-results/junit.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    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'] },
    },
  ],

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

Structure de tests

Organisation fichiers

e2e/
├── auth/
│   ├── login.spec.ts
│   ├── register.spec.ts
│   └── biometric.spec.ts
├── documents/
│   ├── upload.spec.ts
│   ├── download.spec.ts
│   ├── share.spec.ts
│   └── certify.spec.ts
├── fixtures/
│   └── test-data.ts
├── helpers/
│   └── auth-helper.ts
└── pages/          # Page Object Model
    ├── login-page.ts
    ├── dashboard-page.ts
    └── document-page.ts

Pattern Page Object Model (POM)

Exemple : LoginPage

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

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

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.locator('input[name="email"]');
    this.passwordInput = page.locator('input[name="password"]');
    this.loginButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('[role="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.loginButton.click();
  }

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

Utilisation dans test

// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';

test.describe('TC-AUTH: User Authentication', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('TC-AUTH-01: should login with valid credentials', async ({ page }) => {
    await loginPage.login('user@example.com', 'ValidPassword123!');

    // Vérification redirection dashboard
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('h1')).toContainText('Dashboard');
  });

  test('TC-AUTH-02: should show error with invalid credentials', async () => {
    await loginPage.login('user@example.com', 'WrongPassword');

    await loginPage.expectError('Invalid email or password');
    // Reste sur page login
    await expect(loginPage.page).toHaveURL('/login');
  });
});

Sélecteurs robustes

Priorités (du meilleur au pire)

  1. data-testid (recommandé)
  2. role + name (accessible)
  3. label (formulaires)
  4. placeholder (formulaires)
  5. text content (fragile)
  6. CSS classes (très fragile)

Exemples

// ✅ EXCELLENT - data-testid
page.locator('[data-testid="upload-button"]')

// ✅ BON - role + name (accessibilité)
page.getByRole('button', { name: 'Upload Document' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('link', { name: 'Sign Out' })

// ✅ BON - label (formulaires)
page.getByLabel('Email')
page.getByLabel('Password')

// ⚠️ ACCEPTABLE - placeholder
page.getByPlaceholder('Enter your email')

// ⚠️ ACCEPTABLE - text
page.getByText('Welcome back')

// ❌ FRAGILE - CSS classes
page.locator('.btn-primary')  // Peut casser au refactoring CSS

Ajout data-testid dans React

// ✅ Ajouter data-testid pour tests
<button
  data-testid="upload-document-button"
  onClick={handleUpload}
>
  Upload Document
</button>

<input
  data-testid="email-input"
  type="email"
  name="email"
/>

Attentes (Assertions)

Attentes automatiques

Playwright attend automatiquement (timeout: 30s par défaut) :

// ✅ Attend que l'élément soit visible
await expect(page.locator('#username')).toBeVisible();

// ✅ Attend que le texte apparaisse
await expect(page.locator('h1')).toHaveText('Dashboard');

// ✅ Attend que l'élément soit cliquable
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();

// ✅ Attend la navigation
await page.click('a[href="/dashboard"]');
await expect(page).toHaveURL('/dashboard');

Attentes personnalisées

// Attendre un état spécifique
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');

// Attendre un sélecteur
await page.waitForSelector('[data-testid="document-list"]', {
  state: 'visible',
  timeout: 10000,
});

// Attendre une condition
await page.waitForFunction(() => {
  return document.querySelectorAll('.document-item').length > 0;
});

Gestion de l'authentification

Pattern : Réutilisation state auth

// e2e/helpers/auth-helper.ts
import { Page } from '@playwright/test';

export async function authenticate(page: Page, credentials: {
  email: string;
  password: string;
}) {
  await page.goto('/login');
  await page.locator('input[name="email"]').fill(credentials.email);
  await page.locator('input[name="password"]').fill(credentials.password);
  await page.locator('button[type="submit"]').click();

  // Attendre redirection
  await page.waitForURL('/dashboard');

  // Sauvegarder state auth
  const storage = await page.context().storageState();
  return storage;
}

// Utilisation dans tests
test.describe('Documents', () => {
  test.use({
    storageState: 'e2e/.auth/user.json',  // Réutilise auth
  });

  test('TC-DOC-01: should upload document', async ({ page }) => {
    // Déjà authentifié
    await page.goto('/documents');
    // ...
  });
});

Setup global (global-setup.ts)

// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import path from 'path';

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

  // Login
  await page.goto('http://localhost:3000/login');
  await page.locator('input[name="email"]').fill('test@example.com');
  await page.locator('input[name="password"]').fill('TestPassword123!');
  await page.locator('button[type="submit"]').click();
  await page.waitForURL('http://localhost:3000/dashboard');

  // Sauvegarder auth state
  await page.context().storageState({
    path: path.join(__dirname, 'e2e/.auth/user.json'),
  });

  await browser.close();
}

export default globalSetup;

Tests de fichiers (Upload)

test('TC-DOC-01: should upload encrypted document', async ({ page }) => {
  await page.goto('/documents/upload');

  // Upload fichier
  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles('e2e/fixtures/test-document.pdf');

  // Attendre encryption client-side
  await page.waitForSelector('[data-testid="encryption-progress"]');
  await page.waitForSelector('[data-testid="encryption-complete"]');

  // Submit
  await page.getByRole('button', { name: 'Upload' }).click();

  // Vérification succès
  await expect(page.locator('[role="alert"]')).toContainText(
    'Document uploaded successfully'
  );

  // Vérification apparition dans liste
  await page.goto('/documents');
  await expect(page.locator('[data-testid="document-list"]')).toContainText(
    'test-document.pdf'
  );
});

Tests de téléchargement

test('TC-DOC-02: should download encrypted document', async ({ page }) => {
  await page.goto('/documents');

  // Click download
  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Download' }).first().click();

  const download = await downloadPromise;

  // Vérification nom fichier
  expect(download.suggestedFilename()).toMatch(/\.pdf$/);

  // Sauvegarder fichier
  await download.saveAs(`e2e/downloads/${download.suggestedFilename()}`);
});

Tests multi-navigateurs

test.describe('Cross-browser compatibility', () => {
  test('should work on Chromium', async ({ page, browserName }) => {
    test.skip(browserName !== 'chromium', 'Chromium only');
    // Test spécifique Chromium
  });

  test('should work on all browsers', async ({ page }) => {
    // Test sur tous les navigateurs (Chromium, Firefox, WebKit)
    await page.goto('/');
    await expect(page.locator('h1')).toBeVisible();
  });
});

Capture d'artefacts

Screenshots

test('TC-AUTH-01: login flow', async ({ page }) => {
  await page.goto('/login');

  // Screenshot avant action
  await page.screenshot({
    path: 'e2e/screenshots/login-before.png',
    fullPage: true,
  });

  await page.locator('input[name="email"]').fill('user@example.com');
  await page.locator('input[name="password"]').fill('password');
  await page.locator('button[type="submit"]').click();

  // Screenshot après action
  await page.screenshot({
    path: 'e2e/screenshots/login-after.png',
    fullPage: true,
  });
});

Vidéos

// playwright.config.ts
use: {
  video: 'retain-on-failure',  // Vidéo seulement si échec
  // ou
  video: 'on',  // Vidéo pour tous les tests
}

Traces

// playwright.config.ts
use: {
  trace: 'on-first-retry',  // Trace seulement si retry
  // ou
  trace: 'on',  // Trace pour tous les tests
}

// Voir la trace
// npx playwright show-trace trace.zip

Isolation des tests

Règles impératives

  1. Chaque test est indépendant : Aucun test ne dépend d'un autre
  2. State propre : Chaque test commence avec un state vierge
  3. Pas d'ordre : Tests exécutables dans n'importe quel ordre
  4. Cleanup automatique : Browser context recréé entre tests
// ✅ BON - Isolation complète
test.describe('Documents', () => {
  test.beforeEach(async ({ page }) => {
    // Setup avant CHAQUE test
    await page.goto('/documents');
  });

  test('TC-DOC-01: upload', async ({ page }) => {
    // Test isolé
  });

  test('TC-DOC-02: download', async ({ page }) => {
    // Test isolé, ne dépend pas de TC-DOC-01
  });
});

// ❌ MAUVAIS - Dépendance entre tests
test.describe('Documents', () => {
  let documentId: string;

  test('upload', async ({ page }) => {
    // ...
    documentId = await getUploadedDocumentId();  // ❌ State partagé
  });

  test('download', async ({ page }) => {
    await downloadDocument(documentId);  // ❌ Dépend du test précédent
  });
});

Parcours critiques ProbatioVault

TC-AUTH: Authentification

test('TC-AUTH-01: Register + Biometric + Login', async ({ page }) => {
  // 1. Inscription
  await page.goto('/register');
  await page.getByLabel('Email').fill('newuser@example.com');
  await page.getByLabel('Password').fill('SecurePass123!');
  await page.getByRole('button', { name: 'Register' }).click();

  // 2. Configuration biométrie (simulé)
  await expect(page).toHaveURL('/setup-biometric');
  await page.getByRole('button', { name: 'Enable Face ID' }).click();

  // 3. Logout
  await page.getByRole('button', { name: 'Sign Out' }).click();

  // 4. Login avec biométrie
  await page.goto('/login');
  await page.getByLabel('Email').fill('newuser@example.com');
  await page.getByRole('button', { name: 'Sign In with Biometric' }).click();

  // Vérification succès
  await expect(page).toHaveURL('/dashboard');
});

TC-DOC: Documents

test('TC-DOC-01: Full document lifecycle', async ({ page }) => {
  await page.goto('/documents');

  // 1. Upload
  const fileInput = page.locator('input[type="file"]');
  await fileInput.setInputFiles('e2e/fixtures/contract.pdf');
  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.locator('[role="alert"]')).toContainText('uploaded');

  // 2. Visualisation
  await page.getByText('contract.pdf').click();
  await expect(page.locator('iframe')).toBeVisible();  // PDF viewer

  // 3. Partage (PRE)
  await page.getByRole('button', { name: 'Share' }).click();
  await page.getByLabel('Recipient email').fill('recipient@example.com');
  await page.getByRole('button', { name: 'Share Document' }).click();
  await expect(page.locator('[role="alert"]')).toContainText('shared');

  // 4. Certification
  await page.getByRole('button', { name: 'Certify' }).click();
  await expect(page.locator('[data-testid="certified-badge"]')).toBeVisible();
});

Exécution tests

Commandes

# Tous les tests
npx playwright test

# Mode headed (voir browser)
npx playwright test --headed

# Mode debug
npx playwright test --debug

# Tests spécifiques
npx playwright test auth/
npx playwright test login.spec.ts

# Navigateur spécifique
npx playwright test --project=chromium

# Tests en parallèle
npx playwright test --workers=4

# Rapport HTML
npx playwright show-report

Checklist Playwright

Avant commit

  • Tests utilisent Page Object Model
  • Sélecteurs robustes (data-testid ou role)
  • Attentes automatiques (expect)
  • Tests isolés (aucune dépendance)
  • Screenshots sur échec activés
  • Vidéos sur échec activées
  • Tous les tests passent (0 failures)

Escalade

Escalader vers Agent QA (coordinateur) si : - Tests E2E échouent de manière intermittente (flaky) - Impossibilité de sélectionner éléments (sélecteurs fragiles) - Performance tests trop lente (> 30s par test) - Besoin de tests cross-origin (limitations CORS)

Références

  • Playwright Docs: https://playwright.dev/
  • Best Practices: https://playwright.dev/docs/best-practices
  • Page Object Model: https://playwright.dev/docs/pom
  • Selectors: https://playwright.dev/docs/selectors

Historique

Version Date Changement
1.0.0 2026-01-14 Création initiale