TECHNOLOGIES

Playwright, notre outil de tests end-to-end

Playwright est un framework de tests E2E open source développé par Microsoft. Il nous permet de simuler les interactions utilisateurs dans un vrai navigateur et de valider que chaque parcours critique fonctionne correctement.

Chez SmartBooster, Playwright est notre outil de référence pour les tests de bout en bout : couverture des parcours métier, tests multi-navigateurs et intégration dans notre pipeline GitLab CI/CD.

Playwright logo

PRÉSENTATION

Qu'est-ce que Playwright ?

Playwright est un framework de tests end-to-end open source créé par Microsoft. Il pilote de vrais navigateurs (Chromium, Firefox, WebKit) de façon programmatique pour simuler exactement ce qu'un utilisateur ferait sur votre application.

Contrairement aux tests unitaires qui vérifient des fonctions isolées, les tests Playwright valident l'application complète : rendu de l'interface, appels API, transitions de page, gestion des erreurs. C'est la couche de test la plus proche du comportement réel de l'utilisateur.

Playwright s'intègre nativement avec nos projets Vue.js et TypeScript. Il supporte les pages rendues par Vite, les Single Page Applications et les applications hybrides Symfony + Vue.js via Inertia.js.

Logo Playwright

POURQUOI PLAYWRIGHT

Ce qui en fait notre outil de tests E2E de référence

Playwright résout les problèmes classiques des tests end-to-end : tests fragiles, synchronisation difficile, couverture multi-navigateurs coûteuse et débogage opaque.

Validation des parcours critiques

Playwright simule un vrai utilisateur dans le navigateur. Chaque parcours critique — connexion, saisie de formulaire, validation métier — est vérifié automatiquement avant chaque mise en production.

Multi-navigateurs natif

Un seul test, trois moteurs : Chromium, Firefox et WebKit (Safari). Aucune régression ne passe entre navigateurs sans être détectée, sans multiplier le code de test.

Rapidité et fiabilité

Playwright attend automatiquement que les éléments soient interactibles avant d'agir. Pas de sleep arbitraires, pas de tests flaky qui échouent aléatoirement selon la vitesse du serveur CI.

Tests API inclus

Au-delà de l'interface, Playwright peut tester directement les endpoints API de nos applications Symfony. Un outil unique pour les tests UI et les tests d'intégration API, sans jongler entre plusieurs bibliothèques.

TypeScript natif

Les tests Playwright s'écrivent en TypeScript avec une autocomplétion complète. Le même langage que nos composants Vue.js, des types corrects dès l'écriture et des refactorings sûrs.

Captures et traces de débogage

En cas d'échec, Playwright génère une capture d'écran, une vidéo et une trace complète de l'exécution. Le débogage d'un test raté se fait en quelques secondes depuis l'interface Trace Viewer.

NOTRE USAGE

Comment nous utilisons Playwright chez SmartBooster

Nous intégrons Playwright dans les projets Vue.js et Symfony dès lors que des parcours utilisateurs critiques doivent être couverts et que l'application est déployée en continu.

Playwright s'intègre naturellement avec Storybook : Storybook utilise Playwright en interne comme librairie DOM, ce qui rend la base commune pour les tests comportementaux des composants directement dans leurs stories.

Parcours utilisateurs critiques

Nous identifions les parcours à fort enjeu métier (authentification, tunnel de commande, soumission de formulaires complexes) et les couvrons avec des scénarios Playwright. Ces tests constituent le filet de sécurité principal avant chaque déploiement.

Intégration CI/CD GitLab

Les tests Playwright s'exécutent automatiquement dans notre pipeline GitLab CI/CD à chaque push sur la branche principale. Un test en échec bloque le déploiement et alerte l'équipe immédiatement. En pratique, nous nous autorisons à livrer sans relancer l'intégralité des tests lorsque la réactivité prime — la couverture E2E reste un filet, pas un frein.

Tests de régression sur les composants Vue.js

En complément des tests unitaires Vitest et de la documentation interactive Storybook (qui permet également d'exécuter des tests sur les composants isolés), Playwright valide que les composants Vue.js s'affichent et se comportent correctement dans le contexte applicatif complet, avec les vraies données et les vraies interactions.

MIGRATION

Migrer de Cypress à Playwright

Nous utilisions Cypress pour nos tests end-to-end avant de migrer vers Playwright. Le changement a été motivé par trois limites concrètes de Cypress que nous rencontrions au quotidien : l'absence de support WebKit, des pipelines CI lents sur les grosses suites et l'impossibilité d'intégrer nativement les tests avec Storybook.

Tests multi-navigateurs sans surcoût

Cypress ne supporte officiellement que Chromium (Chrome, Edge) et Firefox — WebKit/Safari est à ce jour au stade expérimental. Playwright teste les trois moteurs dans le même runner, sans licence ni service tiers. Une régression Safari est détectée en CI sans changer une ligne de code.

Architecture out-of-process plus rapide

Cypress s'exécute dans le navigateur avec le code de l'application, ce qui engendre des limitations et des lenteurs sur les suites volumineuses. Playwright pilote le navigateur depuis l'extérieur via le protocole CDP, ce qui le rend plus rapide et stable sur les pipelines CI avec de nombreux tests.

Intégration native avec Storybook

Playwright s'intègre directement avec Storybook via @storybook/addon-vitest. Concrètement, chaque story devient un test : Playwright navigue vers la story, la rend dans un vrai navigateur et vérifie qu'elle s'affiche sans erreur. On peut aussi générer des captures d'écran automatiques de chaque composant pour détecter les régressions visuelles entre deux builds, sans outillage visuel dédié supplémentaire.

Migration progressive depuis Cypress

Playwright dispose d'un outil de migration officiel (playwright codegen) qui enregistre les interactions et génère les tests correspondants. Nous avons migré nos suites Cypress existantes en commençant par les parcours critiques, en faisant cohabiter les deux outils le temps de la transition sans bloquer les déploiements.

BONNES PRATIQUES & DOCUMENTATION

Nos conventions Playwright

Documentation de référence pour l'équipe SmartBooster.

Installation des navigateurs pour Playwright dans la CI

Les navigateurs Playwright (Chromium, Firefox, WebKit) pèsent plusieurs centaines de mégaoctets et n'ont rien à faire dans les dépendances du projet. Ils ne servent qu'à l'exécution des tests. Deux approches selon le contexte du projet.

Pourquoi un job CI dédié dans les deux cas ?

  • Les tests E2E sont plus lents que les tests unitaires : les isoler évite de bloquer le pipeline sur un job mixte
  • Les navigateurs ne sont téléchargés ou démarrés que si le job E2E est déclenché, pas à chaque yarn install

Cas 1 : image Docker applicative existante (Symfony + Node)

Le runner utilise déjà une image Docker personnalisée pour le projet. Il faut alors télécharger le navigateur en début de job, juste avant de lancer les tests :

variables:
  # Redirige l'install des binaires Playwright vers un dossier du projet pour pouvoir le mettre en cache GitLab CI
  PLAYWRIGHT_BROWSERS_PATH: "$CI_PROJECT_DIR/.playwright-browsers"

e2e:
  stage: test
  when: manual
  artifacts:
    when: on_failure
    expire_in: 2 days
    paths:
      - test-results/  # screenshots, videos et traces générés par Playwright en cas d'échec
  cache:
    - key: "${CI_PROJECT_ID}_cache"
      paths:
        - node_modules/
    - key: "${CI_PROJECT_ID}_playwright_browsers"
      paths:
        - .playwright-browsers/
  script:
    - yarn install
    - yarn playwright install chromium --with-deps
    - yarn test:e2e:ci

L'option --with-deps installe également les dépendances système requises par Chromium (librairies GTK, NSS...), indispensables sur les runners CI qui partent d'une image Linux/Node minimale.

Cas 2 : projet front Node.js (Vue.js, React...) — image officielle Playwright

Microsoft publie une image Docker officielle avec Chromium, Firefox et WebKit déjà installés et leurs dépendances système pré-configurées. C'est l'approche recommandée pour les projets full front : aucune étape playwright install dans le script, le job démarre directement sur yarn install puis les tests.

La version de l'image doit correspondre exactement à la version de @playwright/test dans package.json.

e2e:
  stage: test
  when: manual
  artifacts:
    when: on_failure
    expire_in: 2 days
    paths:
      - test-results/
  image: mcr.microsoft.com/playwright:v1.59.1-noble
  script:
    - yarn install
    - yarn test:e2e:ci

Script Playwright à ajouter dans le package.json

{
  "scripts": {
    "test:e2e": "playwright test",
    "test:e2e:ci": "playwright test --reporter list",
    "test:e2e:ui": "playwright test --ui",
    "test:e2e:debug": "playwright test --debug",
    "test:e2e:codegen": "playwright codegen http://localhost:8080"
  }
}
  • test:e2e : lance tous les tests E2E en mode headless sur le poste du développeur. Le reporter par défaut génère un rapport HTML complet à l'issue de l'exécution : détail de chaque test, captures d'écran, traces, et bouton "Copy prompt" sur les tests en échec.
  • test:e2e:ci : variante pour la CI. L'option --reporter list remplace le rapport HTML par une sortie stdout ligne par ligne, adaptée aux logs des runners GitLab CI qui ne peuvent pas ouvrir un fichier HTML.
  • test:e2e:ui : ouvre l'interface graphique de Playwright. Elle permet d'explorer les tests, de les rejouer un par un, de voir les captures d'écran et les traces d'exécution. Indispensable pour comprendre pourquoi un test échoue.
  • test:e2e:debug : lance les tests avec le Playwright Inspector ouvert. L'exécution est mise en pause à chaque étape pour inspecter le DOM, les sélecteurs et l'état de la page. Utile pour déboguer un test précis plutôt que de relancer toute la suite.
  • test:e2e:codegen : lance le mode enregistrement sur http://localhost:8080. Playwright ouvre un navigateur et génère automatiquement le code de test correspondant à chaque action effectuée dans l'interface. Point de départ idéal pour créer un nouveau test sans écrire le sélecteur à la main.

Syntaxe pour ne lancer qu'une seul spec de test

Il y a 2 manières de faire :

Cibler directement le fichier de la spec :

yarn test:e2e tests/e2e/specs/parcours_simple.spec.js

Cette version de la commande doit être ajoutée en tête du fichier de spec en commentaire pour simplifier l'exécution du test lors de sa rédaction ou modification.

Ou avec un pattern glob si on souhaite matcher par nom :

yarn test:e2e --grep "Complétion du parcours simple"

--grep filtre par nom de test.describe ou de test(), pratique pour cibler un test précis dans un fichier.

Ces deux formes d'isolation (chemin de fichier et --grep) fonctionnent aussi avec test:e2e:ui : l'interface graphique s'ouvrira en n'affichant que les tests correspondants, ce qui accélère la navigation quand la suite complète est volumineuse.

Comment bien choisir son selecteur d'élément (Locator) du DOM pour ses tests ?

Les locators sont la pièce centrale du système d'attente automatique et de relance (auto-waiting et retry-ability) de Playwright. Concrètement, un locator représente une façon de trouver un ou plusieurs éléments sur la page à n'importe quel moment : à chaque action, Playwright interroge le DOM à nouveau plutôt que de s'appuyer sur une référence capturée au départ.

Choisir le bon locator est important car les deux objectifs d'un bon test E2E sont parfois en tension : représenter au plus près ce que l'utilisateur perçoit et interagi, tout en garantissant un test stable qui ne casse pas à chaque refactoring mineur du HTML.

Recommandation Playwright & SmartBooster : getByRole() est la façon la plus proche de ce que voient l'utilisateur et les technologies d'assistance (lecteurs d'écran). C'est le point de départ par défaut pour tout élément interactif.

Si le besoin de votre test est plus fin, voici l'ordre de priorité recommandé :

Priorité Locator Quand l'utiliser
1 getByRole() Tout élément interactif : bouton, lien, champ, case à cocher. Correspond aux attributs d'accessibilité implicites et explicites.
2 getByLabel() Champs de formulaire associés à un <label>. Reflète exactement ce que l'utilisateur lit avant de saisir.
3 getByPlaceholder() Inputs sans label visible mais avec un placeholder explicite.
4 getByText() Éléments non interactifs (div, span, p) identifiés par leur contenu textuel.
5 getByAltText() Images et éléments avec un attribut alt significatif.
6 getByTitle() Éléments portant un attribut title (info-bulle). Usage peu fréquent.
7 getByTestId() Quand aucun locator sémantique ne suffit, ou quand la stabilité du test prime. Nécessite d'ajouter data-testid dans le code source de l'élément.

getByTestId() pour la stabilité maximale : tester via un attribut data-testid est la méthode la plus résistante aux changements. Même si le texte du bouton ou son rôle évolue, le test passe. En contrepartie, ce locator ne reflète pas ce que l'utilisateur voit réellement. À réserver aux cas où le rôle ou le texte ne suffisent pas, ou quand la stabilité prime sur la lisibilité du test.

Exemple de configuration pour tester avec plusieurs navigateurs

Playwright gère nativement Chromium, Firefox et WebKit (le moteur de Safari) via la clé projects dans playwright.config.ts. Chaque projet est un navigateur indépendant : la même suite de tests s'exécute sur les trois moteurs sans modifier une ligne de code de test.

Les binaires Chromium, Firefox et WebKit sont téléchargés par Playwright lui-même (playwright install). Edge en revanche repose sur le binaire du navigateur installé sur la machine (via channel: 'msedge') et n'est donc pas disponible sur les runners CI Linux standards, d'où son exclusion par défaut.

projects: [
  // Edge réutilise le binaire Chromium (pas d'install séparée), mais nécessite
  // Edge installé sur la machine avec channel: 'msedge'
  // { name: 'edge', use: { ...devices['Desktop Edge'] } },
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
]

Obtenir un premier diagnostic d'erreur de test avec l'IA

Quand un test échoue en mode CLI, Playwright génère automatiquement un rapport HTML et propose de l'ouvrir dans le navigateur. Sur la page du rapport, chaque test en échec dispose d'un bouton "Copy prompt" : en cliquant dessus, Playwright copie dans le presse-papiers un prompt structuré contenant toutes les informations liées à l'erreur (message d'erreur, stack trace, captures d'écran, étapes ayant échoué...).

Ce prompt peut être collé directement dans Claude Code pour obtenir un premier diagnostic sans avoir à extraire manuellement les logs. Claude Code peut identifier la cause probable de l'échec et, si l'erreur est dans le code de test lui-même (mauvais sélecteur, assertion incorrecte, ordre d'étapes inadapté), proposer un début de correction applicable immédiatement.

Pourquoi codegen m'indique `spinbutton` pour mon locator input de type nombre ?

getByRole('spinbutton') est bien le locator correct pour un <input type="number">. Ce rôle n'est pas inventé par Playwright : il correspond au rôle ARIA implicite de tous les champs numériques, tel que défini par la spec WAI-ARIA. Un lecteur d'écran annoncera ce champ comme un "spinbutton" (zone de saisie numérique incrémentale) à ses utilisateurs.

Conformément à l'ordre de priorité des locators décrit plus haut, getByRole est le sélecteur à privilégier car il reflète ce que l'utilisateur et les technologies d'assistance perçoivent réellement. Le codegen suit donc la bonne pratique en proposant ce rôle plutôt qu'un sélecteur CSS fragile comme input[type="number"].

Mocker les appels API externe avec page.route()

Intercepter les appels réseau avec page.route() permet de substituer la réponse d'une API par une donnée contrôlée, sans toucher au serveur réel. Trois raisons principales justifient cette pratique :

  • Éviter le spam des services tiers : une suite E2E qui tourne en CI peut déclencher des dizaines d'appels vers une API de géolocalisation, d'envoi d'email ou de paiement. Le mock isole le test de ces effets de bord et garanti de ne pas se retrouver blacklisté.
  • Indépendance vis-à-vis de l'uptime externe : si l'API tierce est en maintenance ou ralentit, le test ne doit pas échouer pour une raison qui ne lui appartient pas.
  • Contrôle du format de réponse : définir la réponse à l'avance permet de tester des cas précis (ville trouvée, liste vide, erreur 500...) sans dépendre des données réelles du service.

page.route(pattern, handler) intercepte toutes les requêtes dont l'URL correspond au pattern glob fourni. Quand une requête correspond, le handler reçoit un objet route et peut appeler route.fulfill() pour retourner une réponse simulée directement au navigateur, sans qu'aucun appel réseau réel ne soit émis.

Ordre impératif : page.route() doit être enregistré avant l'action qui déclenche l'appel API. Playwright enregistre les handlers dans l'ordre et les applique dès qu'une requête correspondante est émise. Un handler ajouté après l'appel ne sera jamais exécuté pour cette requête.

// Le mock est déclaré EN PREMIER, avant toute interaction avec la page
await page.route('**/geographic/cities/**', route => route.fulfill({
  json: { cities: [{ postalCode: '69001', name: 'LYON 01' }] }
}))

// L'action utilisateur qui déclenche l'appel API vient ensuite
await page.getByPlaceholder('Code postal ou nom de ville').pressSequentially('69001')

Le pattern **/geographic/cities/** utilise la syntaxe glob : ** correspond à n'importe quel segment d'URL, ce qui permet d'intercepter la route quelle que soit l'origine ou les paramètres de chemin autour du segment ciblé.

route.fulfill({ json: ... }) sérialise automatiquement l'objet en JSON et positionne le header Content-Type: application/json.

Mise en place du coverage sur Playwright

Playwright expose une API native page.coverage basée sur le Chrome DevTools Protocol. Elle collecte les données de couverture JavaScript directement dans le navigateur pendant l'exécution des tests, sans modifier le build.

Contrainte majeure : Chromium uniquement. page.coverage n'est pas disponible sur Firefox ni WebKit. Le coverage E2E est donc limité au projet chromium de Playwright, ce qui est suffisant car le coverage mesure quels chemins de code sont exercés — pas la compatibilité cross-browser.

Dépendances

yarn add -D v8-to-istanbul nyc
  • v8-to-istanbul : convertit les données V8 brutes retournées par stopJSCoverage() en format Istanbul
  • nyc : lit les fichiers Istanbul dans .nyc_output/ et génère les rapports finaux (HTML, lcov, cobertura)

Fixture auto (tests/e2e/fixtures.js)

Le coverage est collecté via une fixture auto qui s'exécute autour de chaque test sans modifier les specs. Elle ne s'active que lorsque la variable d'environnement COVERAGE=true est présente.

import { test as base } from '@playwright/test'
import { writeFileSync, mkdirSync } from 'node:fs'
import { randomUUID } from 'node:crypto'
import { resolve } from 'node:path'
import v8toIstanbul from 'v8-to-istanbul'

export const test = base.extend({
  collectCoverage: [async ({ page }, use) => {
    if (process.env.COVERAGE) {
      await page.coverage.startJSCoverage({ resetOnNavigation: false })
    }
    await use()
    if (!process.env.COVERAGE) return
    const entries = await page.coverage.stopJSCoverage()
    const istanbulCoverage = {}
    for (const entry of entries) {
      if (!entry.url.includes('/src/') || !entry.source) continue
      const filePath = resolve(process.cwd(), entry.url.replace(/^https?:\/\/[^/]+\//, ''))
      const converter = v8toIstanbul(filePath, 0, { source: entry.source })
      await converter.load()
      converter.applyCoverage(entry.functions)
      Object.assign(istanbulCoverage, converter.toIstanbul())
    }
    if (Object.keys(istanbulCoverage).length > 0) {
      mkdirSync('.nyc_output', { recursive: true })
      writeFileSync(`.nyc_output/${randomUUID()}.json`, JSON.stringify(istanbulCoverage))
    }
  }, { auto: true }]
})

export { expect } from '@playwright/test'

Points clés :

  • resetOnNavigation: false est indispensable si votre projet est une app de type SPA avec vue-router : sans cette option, les données de coverage seraient réinitialisées à chaque changement de route pendant un test
  • Le filtre !/src/ exclut les scripts Vite internes (@vite/client), les chunks node_modules et les scripts inline sans lien avec le code applicatif
  • randomUUID() garantit un nom de fichier unique par test même avec fullyParallel: true et plusieurs workers
  • Les specs importent test et expect depuis ../fixtures.js au lieu de @playwright/test

Configuration du rapport (.nycrc.json)

{
  "reporter": ["html", "text", "lcov", "cobertura"],
  "include": ["src/**"],
  "exclude": ["src/**/*.stories.{js,vue}", "src/stories/**"],
  "report-dir": "coverage/e2e"
}
  • html : rapport navigable dans coverage/e2e/index.html
  • text : résumé affiché dans la sortie stdout du CI (nécessaire pour la regex GitLab)
  • lcov : format standard pour les outils tiers
  • cobertura : format lu par GitLab pour les annotations de coverage inline dans les diffs de MR

Scripts package.json

"test:e2e:coverage-chromium": "rm -rf .nyc_output && COVERAGE=true playwright test --reporter list --project=chromium",
"test:e2e:coverage-report": "nyc report"

Le rm -rf .nyc_output en tête de commande est important : sans lui, nyc report mergerait les fichiers d'un run précédent avec les nouveaux et fausserait le calcul du coverage.

Job GitLab CI (test-e2e-coverage-chromium)

test-e2e-coverage-chromium:
    stage: test
    image: mcr.microsoft.com/playwright:v1.59.1-noble
    when: manual
    coverage: /All files\s*\|\s*(\d+\.?\d*)/
    artifacts:
        when: always
        expire_in: 2 days
        paths:
            - test-results/
            - coverage/e2e/
        reports:
            coverage_report:
                coverage_format: cobertura
                path: coverage/e2e/cobertura-coverage.xml
    script:
        - yarn install
        - yarn test:e2e:coverage-chromium
        - yarn test:e2e:coverage-report
  • coverage: : regex qui extrait le pourcentage global depuis la sortie text de nyc (All files | 67.5 | ...) et l'affiche dans l'UI pipeline et sur les MR GitLab
  • artifacts: when: always : le rapport HTML et le XML cobertura sont uploadés même si les tests passent
  • coverage_report: cobertura : active les annotations de coverage inline dans les diffs de MR GitLab
  • Le job est when: manual car si vous avez beaucoup de test E2E il n'est pas nécessaire de lancer ce job automatiquement

Pour aller plus loin

Documentation utile

Playwright : Site officiel

Documentation complète, guides de démarrage et référence API pour Playwright.

Playwright : Getting Started

Guide d'installation et premiers tests en TypeScript, avec configuration recommandée.

Pour aller plus loin

Approfondir votre réflexion

Vitest

Playwright couvre les tests E2E, Vitest couvre les tests unitaires Vue.js. Ensemble, ils forment une stratégie de test complète sur nos projets frontend.

Vue.js

Playwright valide les parcours utilisateurs de nos applications Vue.js en conditions réelles de navigation, en complément des tests unitaires Vitest.

GitLab CI/CD

Les tests Playwright s'exécutent automatiquement dans notre pipeline GitLab. Un échec bloque le déploiement et garantit la qualité en production.

Storybook

Playwright s'intègre avec Storybook pour générer des captures d'écran des composants et détecter les régressions visuelles directement depuis la documentation interactive.