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
- Pourquoi Playwright + TypeScript ?
- Installation et configuration
- Patterns de test essentiels
- Tests d'interface utilisateur avancés
- Tests API intégrés
- Tests de régression visuelle
- Utilitaires et helpers
- CI/CD et rapports
- Bonnes pratiques et optimisation
- Vue d'ensemble de l'architecture
- Conclusion
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 ! 🚀