← Retour à la liste

Maîtrisez les Tests E2E avec Playwright et TypeScript : Guide Complet 2025

Introduction

Dans le paysage du développement web moderne, la qualité logicielle n'est plus une option mais une nécessité absolue. Les tests end-to-end (E2E) représentent la dernière ligne de défense contre les bugs qui pourraient compromettre l'expérience utilisateur et la réputation de votre application.

Playwright, combiné à TypeScript, révolutionne l'approche des tests automatisés en offrant une solution à la fois puissante et élégante. Cette combinaison technologique permet aux équipes de développement de créer des suites de tests fiables, maintenables et ultra-rapides qui s'intègrent parfaitement dans les workflows modernes de CI/CD.

Ce guide complet vous accompagne dans la maîtrise de Playwright avec TypeScript, de l'installation initiale aux patterns avancés utilisés par les équipes techniques de pointe. Vous découvrirez comment transformer vos tests E2E en un véritable accélérateur de développement plutôt qu'en un frein à la productivité.

Sommaire

Automatisez vos tests end-to-end avec Playwright et TypeScript. Cette combinaison puissante vous permet de créer des tests fiables, maintenables et ultra-rapides pour vos applications web modernes.

Pourquoi Playwright + TypeScript ?

Playwright se distingue des autres outils de test par sa rapidité, sa fiabilité et son support multi-navigateurs natif. Combiné à TypeScript, il offre une expérience développeur exceptionnelle.

Avantages clés

  • Multi-navigateurs : Chrome, Firefox, Safari et Edge
  • Auto-wait : Attente intelligente des éléments
  • Parallélisation : Tests simultanés pour une vitesse maximale
  • TypeScript natif : IntelliSense et détection d'erreurs
  • Screenshots/Videos : Debugging visuel intégré

Installation et configuration

Setup initial

# Installation
npm init playwright@latest
npm install --save-dev @playwright/test typescript

# Structure générée
playwright.config.ts
tests/
├── example.spec.ts
└── test-examples/

Configuration TypeScript optimale

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  // Dossier des tests
  testDir: "./tests",

  // Timeout global
  timeout: 30000,
  expect: {
    timeout: 5000,
  },

  // Configuration par défaut
  use: {
    baseURL: "http://localhost:3000",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },

  // Projets multi-navigateurs
  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"] },
    },
  ],

  // Serveur de développement
  webServer: {
    command: "npm run dev",
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },

  // Rapports
  reporter: [
    ["html"],
    ["json", { outputFile: "test-results.json" }],
    ["junit", { outputFile: "results.xml" }],
  ],
});

Patterns de test essentiels

Page Object Model moderne

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

export class LoginPage {
  private readonly page: Page;
  private readonly emailInput: Locator;
  private readonly passwordInput: Locator;
  private readonly loginButton: Locator;
  private 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.loginButton = page.locator('[data-testid="login-button"]');
    this.errorMessage = page.locator('[data-testid="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.loginButton.click();
  }

  async expectLoginSuccess() {
    await expect(this.page).toHaveURL("/dashboard");
  }

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

  // Méthodes utilitaires
  async isLoginButtonEnabled(): Promise<boolean> {
    return await this.loginButton.isEnabled();
  }

  async clearForm() {
    await this.emailInput.clear();
    await this.passwordInput.clear();
  }
}

Tests avec fixtures personnalisées

// fixtures/auth.ts
import { test as base, Page } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";

type TestFixtures = {
  loginPage: LoginPage;
  authenticatedPage: Page;
};

export const test = base.extend<TestFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  },

  authenticatedPage: async ({ page, loginPage }, use) => {
    await loginPage.goto();
    await loginPage.login("admin@example.com", "password123");
    await loginPage.expectLoginSuccess();
    await use(page);
  },
});

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

Suite de tests authentification

// tests/auth.spec.ts
import { test, expect } from "../fixtures/auth";

test.describe("Authentification", () => {
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto();
  });

  test("connexion réussie avec identifiants valides", async ({ loginPage }) => {
    await loginPage.login("user@example.com", "validpassword");
    await loginPage.expectLoginSuccess();
  });

  test("échec de connexion avec mot de passe incorrect", async ({
    loginPage,
  }) => {
    await loginPage.login("user@example.com", "wrongpassword");
    await loginPage.expectLoginError("Identifiants incorrects");
  });

  test("validation des champs requis", async ({ loginPage, page }) => {
    await loginPage.login("", "");

    // Vérifier que le bouton reste désactivé
    expect(await loginPage.isLoginButtonEnabled()).toBeFalsy();

    // Vérifier les messages de validation HTML5
    const emailInput = page.locator('[data-testid="email-input"]');
    await expect(emailInput).toHaveAttribute("required");
  });

  test("redirection après connexion réussie", async ({ loginPage, page }) => {
    // Simuler une redirection depuis une page protégée
    await page.goto("/dashboard");
    await expect(page).toHaveURL("/login?redirect=/dashboard");

    await loginPage.login("user@example.com", "validpassword");
    await expect(page).toHaveURL("/dashboard");
  });
});

Tests d'interface utilisateur avancés

Interactions complexes

// tests/ui-interactions.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Interactions UI", () => {
  test("drag and drop dans une liste", async ({ page }) => {
    await page.goto("/kanban");

    const taskCard = page.locator('[data-testid="task-1"]');
    const targetColumn = page.locator('[data-testid="column-done"]');

    // Drag & Drop
    await taskCard.dragTo(targetColumn);

    // Vérifier que la tâche a changé de colonne
    await expect(targetColumn.locator('[data-testid="task-1"]')).toBeVisible();
  });

  test("upload de fichier", async ({ page }) => {
    await page.goto("/upload");

    const fileInput = page.locator('input[type="file"]');

    // Upload d'un fichier de test
    await fileInput.setInputFiles("./test-data/sample.pdf");

    // Vérifier l'upload
    await expect(page.locator(".upload-success")).toBeVisible();
    await expect(page.locator(".file-name")).toContainText("sample.pdf");
  });

  test("géolocalisation", async ({ page, context }) => {
    // Mock de la géolocalisation
    await context.setGeolocation({ latitude: 48.8566, longitude: 2.3522 });
    await context.grantPermissions(["geolocation"]);

    await page.goto("/map");
    await page.click('[data-testid="locate-button"]');

    // Vérifier que la carte s'est centrée sur Paris
    await expect(page.locator(".location-info")).toContainText("Paris");
  });
});

Tests de performance

// tests/performance.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Performance", () => {
  test("temps de chargement de la page d'accueil", async ({ page }) => {
    const startTime = Date.now();

    await page.goto("/");

    // Attendre que le contenu principal soit chargé
    await page.waitForSelector('[data-testid="main-content"]');

    const loadTime = Date.now() - startTime;

    // Vérifier que la page se charge en moins de 3 secondes
    expect(loadTime).toBeLessThan(3000);
  });

  test("metrics Web Vitals", async ({ page }) => {
    await page.goto("/");

    // Attendre que la page soit complètement chargée
    await page.waitForLoadState("networkidle");

    // Récupérer les métriques de performance
    const metrics = await page.evaluate(() => {
      return new Promise((resolve) => {
        new PerformanceObserver((list) => {
          const entries = list.getEntries();
          const vitals = {};

          entries.forEach((entry) => {
            if (entry.entryType === "largest-contentful-paint") {
              vitals.lcp = entry.startTime;
            }
            if (entry.entryType === "first-input") {
              vitals.fid = entry.processingStart - entry.startTime;
            }
          });

          resolve(vitals);
        }).observe({ entryTypes: ["largest-contentful-paint", "first-input"] });

        // Timeout après 5 secondes
        setTimeout(() => resolve({}), 5000);
      });
    });

    console.log("Web Vitals:", metrics);
  });
});

Tests API intégrés

Mock et interception

// tests/api-mocking.spec.ts
import { test, expect } from "@playwright/test";

test.describe("API Mocking", () => {
  test("mock de l'API utilisateurs", async ({ page }) => {
    // Intercepter et mocker les appels API
    await page.route("/api/users", async (route) => {
      const mockUsers = [
        { id: 1, name: "John Doe", email: "john@example.com" },
        { id: 2, name: "Jane Smith", email: "jane@example.com" },
      ];

      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify(mockUsers),
      });
    });

    await page.goto("/users");

    // Vérifier que les données mockées s'affichent
    await expect(page.locator('[data-testid="user-1"]')).toContainText(
      "John Doe"
    );
    await expect(page.locator('[data-testid="user-2"]')).toContainText(
      "Jane Smith"
    );
  });

  test("simulation d'erreur API", async ({ page }) => {
    // Simuler une erreur 500
    await page.route("/api/users", async (route) => {
      await route.fulfill({
        status: 500,
        body: "Internal Server Error",
      });
    });

    await page.goto("/users");

    // Vérifier l'affichage de l'erreur
    await expect(page.locator(".error-message")).toBeVisible();
    await expect(page.locator(".error-message")).toContainText(
      "Erreur de chargement"
    );
  });

  test("test de l'état de chargement", async ({ page }) => {
    // Retarder la réponse API
    await page.route("/api/users", async (route) => {
      await new Promise((resolve) => setTimeout(resolve, 2000));
      await route.continue();
    });

    const responsePromise = page.waitForResponse("/api/users");
    await page.goto("/users");

    // Vérifier que le loader s'affiche
    await expect(page.locator(".loading-spinner")).toBeVisible();

    await responsePromise;

    // Vérifier que le loader disparaît
    await expect(page.locator(".loading-spinner")).not.toBeVisible();
  });
});

Tests de régression visuelle

Screenshots automatisés

// tests/visual-regression.spec.ts
import { test, expect } from "@playwright/test";

test.describe("Régression visuelle", () => {
  test("page d'accueil - desktop", async ({ page }) => {
    await page.goto("/");
    await page.waitForSelector('[data-testid="hero-section"]');

    // Screenshot de la page complète
    await expect(page).toHaveScreenshot("homepage-desktop.png");
  });

  test("page d'accueil - mobile", async ({ page }) => {
    await page.setViewportSize({ width: 375, height: 667 });
    await page.goto("/");
    await page.waitForSelector('[data-testid="hero-section"]');

    await expect(page).toHaveScreenshot("homepage-mobile.png");
  });

  test("composant carte - états multiples", async ({ page }) => {
    await page.goto("/components/card");

    const card = page.locator('[data-testid="demo-card"]');

    // État normal
    await expect(card).toHaveScreenshot("card-normal.png");

    // État hover
    await card.hover();
    await expect(card).toHaveScreenshot("card-hover.png");

    // État focus
    await card.focus();
    await expect(card).toHaveScreenshot("card-focus.png");
  });

  test("thème sombre", async ({ page }) => {
    await page.goto("/");

    // Activer le thème sombre
    await page.click('[data-testid="theme-toggle"]');
    await page.waitForSelector('[data-theme="dark"]');

    await expect(page).toHaveScreenshot("homepage-dark-theme.png");
  });
});

Utilitaires et helpers

Helpers personnalisés

// utils/test-helpers.ts
import { Page, expect } from "@playwright/test";

export class TestHelpers {
  constructor(private page: Page) {}

  // Attendre qu'un élément soit chargé et visible
  async waitForElementWithContent(selector: string, text: string) {
    await this.page.waitForSelector(selector);
    await expect(this.page.locator(selector)).toContainText(text);
  }

  // Remplir un formulaire dynamiquement
  async fillForm(formData: Record<string, string>) {
    for (const [field, value] of Object.entries(formData)) {
      await this.page.fill(`[data-testid="${field}"]`, value);
    }
  }

  // Attendre la fin des requêtes réseau
  async waitForNetworkIdle() {
    await this.page.waitForLoadState("networkidle");
  }

  // Simuler une frappe lente (utile pour les autocompletes)
  async typeSlowly(selector: string, text: string, delay = 100) {
    const element = this.page.locator(selector);
    await element.clear();

    for (const char of text) {
      await element.type(char);
      await this.page.waitForTimeout(delay);
    }
  }

  // Vérifier qu'aucune erreur JavaScript n'est apparue
  async expectNoJSErrors() {
    const errors = await this.page.evaluate(() => {
      return window.errors || [];
    });

    expect(errors).toHaveLength(0);
  }

  // Screenshot avec masquage d'éléments dynamiques
  async screenshotWithMask(name: string, maskSelectors: string[] = []) {
    await expect(this.page).toHaveScreenshot(name, {
      mask: maskSelectors.map((sel) => this.page.locator(sel)),
    });
  }
}

Configuration des données de test

// test-data/users.ts
export const TEST_USERS = {
  admin: {
    email: "admin@example.com",
    password: "admin123",
    role: "admin",
  },
  user: {
    email: "user@example.com",
    password: "user123",
    role: "user",
  },
  blocked: {
    email: "blocked@example.com",
    password: "blocked123",
    role: "user",
    status: "blocked",
  },
} as const;

// test-data/products.ts
export const TEST_PRODUCTS = [
  {
    id: 1,
    name: "MacBook Pro",
    price: 2499,
    category: "laptop",
  },
  {
    id: 2,
    name: "iPhone 15",
    price: 999,
    category: "phone",
  },
] as const;

CI/CD et rapports

Configuration GitHub Actions

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

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

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

Bonnes pratiques et optimisation

Stratégies de test efficaces

// Organisation des tests par criticité
test.describe("Tests critiques", () => {
  test.use({ trace: "on" }); // Trace complète pour debug

  // Tests de parcours utilisateur essentiels
});

test.describe("Tests de régression", () => {
  test.use({
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  });

  // Tests plus légers pour détecter les régressions
});

Parallélisation intelligente

// playwright.config.ts
export default defineConfig({
  // Nombre de workers basé sur les ressources
  workers: process.env.CI ? 2 : undefined,

  // Répartition des tests
  fullyParallel: true,

  // Retry en cas d'échec
  retries: process.env.CI ? 2 : 0,

  // Optimisations
  use: {
    // Réutiliser les onglets
    reuseExistingContext: true,
  },
});

Vue d'ensemble de l'architecture

Conclusion

Playwright avec TypeScript offre un environnement de test E2E robuste et moderne. Cette combinaison permet de détecter les bugs avant qu'ils n'atteignent la production tout en maintenant une vitesse de développement élevée.

Points clés à retenir

  • Page Object Model : Structure et réutilisabilité
  • Fixtures personnalisées : Setup automatisé et isolation
  • Auto-wait : Fiabilité sans timeouts manuels
  • Tests visuels : Détection des régressions UI
  • Mocking intelligent : Tests indépendants des APIs

Métriques de succès

  • Temps d'exécution < 10 minutes pour la suite complète
  • Taux de faux positifs < 5%
  • Couverture des parcours critiques à 100%
  • Maintenance minimale grâce aux bonnes pratiques

Avec ces patterns et techniques, vos tests E2E deviendront un atout précieux pour la qualité et la vélocité de votre équipe ! 🚀