Page Object Model en Playwright : faut-il encore l'utiliser en 2026 ?

Le Page Object Model (POM) est l'un des patterns les plus enseignés en QA. Mais avec Playwright et son API moderne, est-ce qu'il garde tout son sens ? Spoiler : oui, mais pas comme on le pratiquait à l'époque de Selenium. Tour d'horizon des approches modernes.

1. Le Page Object Model, d'où ça vient

Le POM est un pattern formalisé par Martin Fowler vers 2013, popularisé par la communauté Selenium. L'idée : encapsuler les interactions avec une page web dans une classe dédiée, pour éviter que les sélecteurs CSS soient éparpillés dans tous les tests.

À l'époque, Selenium imposait un code très verbeux. Le POM était une vraie bouée de sauvetage : il permettait de centraliser les By.id(), By.cssSelector(), et de réutiliser des actions comme "se connecter" ou "ajouter au panier".

2. Un exemple de POM classique en Playwright

Voilà à quoi ressemble une implémentation POM standard :

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

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

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByTestId("login-email");
    this.passwordInput = page.getByTestId("login-password");
    this.submitBtn = page.getByTestId("login-submit");
  }

  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.submitBtn.click();
  }
}

Et l'utilisation dans un test :

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

test("login happy path", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login("jane@example.com", "****");
  await expect(page).toHaveURL("/dashboard");
});

Lisible, encapsulé, réutilisable. Sur le papier, c'est parfait.

3. Les limites du POM dans la vraie vie

Après avoir maintenu des POM sur des projets de plus de 100 tests, plusieurs problèmes émergent :

1. Sur-abstraction prématurée

On crée une classe par page "au cas où". Mais souvent, une page n'a qu'un seul test, et le POM ajoute juste de la friction sans valeur réelle.

2. Couplage des locators et des méthodes

Le POM mélange "comment trouver l'élément" et "comment l'utiliser". Quand on veut changer une assertion, on doit toucher la page object - même si on ne change pas la sélection.

3. Inheritance hell

On finit avec BasePageAuthenticatedPageDashboardPageSettingsPage. Les changements en haut de l'arbre cassent tout en bas.

4. Page objects géants

Sur une page complexe (un dashboard avec 10 widgets), la classe explose : 50 locators, 30 méthodes. Difficile à lire, encore plus à maintenir.

5. Frottement avec les locators chaînés Playwright

Playwright propose un système de locators ultra-puissant (page.getByRole(), filter(), nth()) qui s'accommode mal du POM rigide. On finit par ne pas les utiliser.

4. Alternative moderne : les fixtures Playwright

Playwright fournit un système de fixtures custom qui permet d'injecter des "objets de test" directement dans les tests, sans classe ni constructeur.

// fixtures.ts
import { test as base, expect } from "@playwright/test";

type Fixtures = {
  loggedInPage: import("@playwright/test").Page;
};

export const test = base.extend<Fixtures>({
  loggedInPage: async ({ page }, use) => {
    await page.goto("/login");
    await page.getByTestId("login-email").fill("jane@example.com");
    await page.getByTestId("login-password").fill(process.env.PW_TEST_PWD!);
    await page.getByTestId("login-submit").click();
    await page.waitForURL("/dashboard");
    await use(page);
  },
});

export { expect };

Et les tests deviennent :

import { test, expect } from "./fixtures";

test("acces dashboard", async ({ loggedInPage }) => {
  await expect(loggedInPage.getByTestId("welcome-banner")).toBeVisible();
});

Avantages : pas de classe, pas d'instanciation, le test exprime directement son intention. Et la fixture peut faire du setup / teardown automatique.

5. Alternative moderne : helpers fonctionnels

Pour les actions transverses (login, ajout d'un produit au panier), on peut simplement écrire des fonctions :

// helpers/auth.ts
export async function loginAs(page: Page, user: { email: string; password: string }) {
  await page.goto("/login");
  await page.getByTestId("login-email").fill(user.email);
  await page.getByTestId("login-password").fill(user.password);
  await page.getByTestId("login-submit").click();
  await page.waitForURL("/dashboard");
}

export async function addToCart(page: Page, sku: string, qty = 1) {
  await page.goto("/products/" + sku);
  if (qty > 1) await page.getByTestId("qty-input").fill(String(qty));
  await page.getByTestId("add-to-cart").click();
}

Plus léger qu'un POM, plus testable, plus composable. C'est le pattern qu'on utilise majoritairement chez Prod Watch.

6. L'approche hybride pragmatique

La vérité, c'est qu'aucune approche n'est universellement bonne. Voici la matrice qu'on recommande :

Cas d'usage Approche recommandée
Action transverse (login, signup) Helper fonctionnel ou fixture Playwright
Setup d'état utilisé par plusieurs tests (utilisateur loggé, panier rempli) Fixture Playwright
Page complexe avec beaucoup d'éléments interactifs POM léger (locators + 5-10 méthodes max)
Page utilisée dans 1-2 tests Locators inline, pas d'abstraction
Composant UI réutilisé sur plusieurs pages (ex : modal) Component Object Model (mini POM scoped)

Component Object Model

C'est la version moderne et utile du POM : on n'encapsule plus une page entière, mais un composant UI réutilisé. Exemple :

// components/ProductCard.ts
export class ProductCard {
  constructor(private root: Locator) {}

  async addToCart() {
    await this.root.getByTestId("add-to-cart").click();
  }

  title() { return this.root.getByTestId("product-title"); }
  price() { return this.root.getByTestId("product-price"); }
}

// dans le test
const firstCard = new ProductCard(page.getByTestId("product-card").first());
await firstCard.addToCart();
await expect(firstCard.title()).toHaveText("T-shirt premium");

Léger, scoped, réutilisable sur toutes les pages qui montrent ce composant. C'est ça, le POM utile en 2026.

7. Verdict 2026

Ne jetez pas le POM, mais arrêtez d'en faire systématiquement. Utilisez les fixtures pour l'état, les helpers pour les actions transverses, et le pattern Component Object pour les UI réutilisés. Les Page Objects "1 page = 1 classe" sont rarement la meilleure réponse.

Notre stack pédagogique chez Prod Watch :

  1. 1 fichier fixtures.ts pour les états récurrents (loggedIn, withCart, asAdmin).
  2. 1 dossier helpers/ pour les actions transverses pures.
  3. 1 dossier components/ pour les composants UI réutilisés (cards, modals, sliders).
  4. Pas de dossier pages/ sauf cas très spécifique.

Cette approche réduit en moyenne de 30% le code de tests sur les projets que nous reprenons, tout en améliorant la lisibilité.

Vue d'ensemble : ce sujet fait partie de notre guide complet sur les tests automatisés en 2026 qui couvre frameworks, IA, no-code, ROI, architecture et monitoring synthétique en un seul article.

Auditez votre suite Playwright avec nous

Diagnostic gratuit de votre architecture de tests + recommandations actionables sous 48h.