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 BasePage → AuthenticatedPage → DashboardPage → SettingsPage. 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
Notre stack pédagogique chez Prod Watch :
- 1 fichier
fixtures.tspour les états récurrents (loggedIn, withCart, asAdmin). - 1 dossier
helpers/pour les actions transverses pures. - 1 dossier
components/pour les composants UI réutilisés (cards, modals, sliders). - 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é.
Auditez votre suite Playwright avec nous
Diagnostic gratuit de votre architecture de tests + recommandations actionables sous 48h.