Tests fondamentaux - 06 - Tests e2e avec Playwright

Écrire des tests end-to-end avec Playwright. Navigation, clicks, formulaires, assertions et debug.

06 - Tests e2e avec Playwright

Ce que tu vas apprendre

  • Installer et configurer Playwright
  • Écrire des tests qui naviguent, cliquent, remplissent des formulaires
  • Utiliser des sélecteurs accessibles (getByRole, getByLabel)
  • Debugger un test qui echoue avec traces et screenshots
  • Le pattern Page Object Model

Prerequisites

Avoir Node.js ou Bun installe. Avoir lu les articles précédents.


Pourquoi Playwright

Playwright est le meilleur outil de test e2e que j'ai utilise. Il est rapide, fiable, et son API est un plaisir a utiliser. Contrairement a Cypress, il supporte tous les navigateurs (Chromium, Firefox, WebKit), il tourne en parallèle par défaut, et il n'a pas cette architecture bizarre de proxy.

Pour les tests e2e de paltemps.fr, c'est mon choix depuis le début. Pas une seconde de regret.

Installation

bashbun add -d @playwright/test
bunx playwright install

La deuxieme commande telecharge les navigateurs. C'est long la première fois (environ 400 Mo), mais ca ne se fait qu'une fois.

Configuration minimale

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

export default defineConfig({
  testDir: "./e2e",
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: "http://localhost:3000",
    screenshot: "only-on-failure",
    trace: "on-first-retry",
  },
});

Mets tes tests dans un dossier e2e/ pour les séparer des tests unitaires. Bun ne doit pas essayer de les lancer avec bun test.

Un premier test

typescript// e2e/homepage.spec.ts
import { test, expect } from "@playwright/test";

test("homepage displays site title", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByRole("heading", { level: 1 })).toHaveText("Pal'Temps");
});

Lance-le :

bashbunx playwright test

C'est tout. Playwright lance un navigateur headless, charge la page, vérifié le titre, ferme le navigateur. En 2-3 secondes.

Selecteurs : oublie les CSS selectors

Playwright encourage les sélecteurs accessibles. Pas de page.click('.btn-primary') ni de page.querySelector('#submit-form'). Ces sélecteurs sont fragiles : un changement de classe CSS casse le test.

A la place :

typescript// Par role (le meilleur)
page.getByRole("button", { name: "Connexion" });
page.getByRole("textbox", { name: "Email" });
page.getByRole("link", { name: "Mon compte" });
page.getByRole("heading", { name: "Dashboard" });

// Par label (pour les formulaires)
page.getByLabel("Mot de passe");
page.getByLabel("Adresse email");

// Par texte visible
page.getByText("Votre commande est confirmee");

// Par placeholder (acceptable)
page.getByPlaceholder("Rechercher...");

// Par test-id (dernier recours)
page.getByTestId("order-total");

Ma hiérarchie : getByRole > getByLabel > getByText > getByPlaceholder > getByTestId. Le getByRole est le plus stable parce qu'il correspond a ce que l'utilisateur voit et fait. Si tu changes le texte du bouton de "Connexion" a "Se connecter", c'est normal que le test casse : l'interface a change.

Tester un flow de login

Voici un test complet qui simule un utilisateur qui se connecte et vérifié le dashboard :

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

test("user can log in and see dashboard", async ({ page }) => {
  // Aller sur la page de login
  await page.goto("/login");

  // Remplir le formulaire
  await page.getByLabel("Email").fill("admin@paltemps.fr");
  await page.getByLabel("Mot de passe").fill("supersecret");

  // Soumettre
  await page.getByRole("button", { name: "Connexion" }).click();

  // Verifier la redirection
  await expect(page).toHaveURL("/dashboard");

  // Verifier le contenu du dashboard
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
  await expect(page.getByText("Bienvenue, Admin")).toBeVisible();
});

test("shows error on invalid credentials", async ({ page }) => {
  await page.goto("/login");
  await page.getByLabel("Email").fill("wrong@example.com");
  await page.getByLabel("Mot de passe").fill("wrongpassword");
  await page.getByRole("button", { name: "Connexion" }).click();

  await expect(page.getByText("Identifiants incorrects")).toBeVisible();
  await expect(page).toHaveURL("/login"); // on reste sur login
});

Note qu'il n'y a aucun waitForTimeout ou sleep. Playwright attend automatiquement que les éléments apparaissent. Quand tu ecris await expect(element).toBeVisible(), Playwright reessaye pendant 5 secondes (configurable). Pas besoin de gerer les timings manuellement.

Formulaire complet avec validation

typescript// e2e/register.spec.ts
import { test, expect } from "@playwright/test";

test("user registration with validation", async ({ page }) => {
  await page.goto("/register");

  // Soumettre vide pour voir les erreurs
  await page.getByRole("button", { name: "Creer mon compte" }).click();
  await expect(page.getByText("L'email est requis")).toBeVisible();

  // Remplir avec un email invalide
  await page.getByLabel("Email").fill("pas-un-email");
  await page.getByRole("button", { name: "Creer mon compte" }).click();
  await expect(page.getByText("Email invalide")).toBeVisible();

  // Remplir correctement
  await page.getByLabel("Email").fill("nouveau@example.com");
  await page.getByLabel("Mot de passe").fill("monMotDePasse123");
  await page.getByLabel("Confirmer le mot de passe").fill("monMotDePasse123");
  await page.getByRole("button", { name: "Creer mon compte" }).click();

  // Redirection vers le dashboard
  await expect(page).toHaveURL("/dashboard");
});

Le Page Object Model

Quand tes tests grandissent, tu vas répéter les memes actions : se connecter, naviguer vers une page, remplir un formulaire. Le Page Object Model (POM) centralise ces interactions.

typescript// e2e/pages/login-page.ts
import { Page, expect } from "@playwright/test";

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

  async goto() {
    await this.page.goto("/login");
  }

  async login(email: string, password: string) {
    await this.page.getByLabel("Email").fill(email);
    await this.page.getByLabel("Mot de passe").fill(password);
    await this.page.getByRole("button", { name: "Connexion" }).click();
  }

  async expectError(message: string) {
    await expect(this.page.getByText(message)).toBeVisible();
  }
}
typescript// e2e/pages/dashboard-page.ts
import { Page, expect } from "@playwright/test";

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

  async expectWelcome(name: string) {
    await expect(this.page.getByText(`Bienvenue, ${name}`)).toBeVisible();
  }

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

Et le test devient :

typescript// e2e/login-pom.spec.ts
import { test } from "@playwright/test";
import { LoginPage } from "./pages/login-page";
import { DashboardPage } from "./pages/dashboard-page";

test("login flow with POM", async ({ page }) => {
  const loginPage = new LoginPage(page);
  const dashboard = new DashboardPage(page);

  await loginPage.goto();
  await loginPage.login("admin@paltemps.fr", "supersecret");
  await dashboard.expectURL();
  await dashboard.expectWelcome("Admin");
});

Quand l'UI change (un label renomme, un bouton deplace), tu corriges une seule classe, pas 15 tests.

Debug : quand ca casse

bash# Mode UI : tu vois le navigateur en direct
bunx playwright test --ui

# Mode headed : le navigateur s'affiche
bunx playwright test --headed

# Mode debug : pas a pas avec inspector
bunx playwright test --debug

# Voir le rapport HTML apres les tests
bunx playwright show-report

Le mode --ui est mon préféré. Tu vois chaque action se rejouer, tu peux mettre en pause, inspecter le DOM, regarder les screenshots. C'est comme un replay video de ton test.

Les traces (trace: "on-first-retry" dans la config) enregistrent tout : le DOM, les requêtes réseau, les screenshots. Quand un test echoue en CI, tu telecharges la trace et tu vois exactement ce qui s'est passe.

Combien de tests e2e ?

Rappelle-toi la pyramide de l'article 01 : 10% de tests e2e, pas plus. Teste les parcours critiques :

  • Login/logout
  • Inscription
  • Le flow d'achat complet
  • Les actions destructives (suppression de compte)

Ne teste PAS chaque variation de formulaire en e2e. Pour ca, les tests unitaires et fonctionnels sont bien plus efficaces.


Article précédent : 05 - Tests fonctionnels Article suivant : 07 - Tests de contrat d'API

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.