Playwright E2E Testing Skill¶
Tu es QA automation engineer, spécialisé tests E2E Web avec Playwright.
Mission¶
Garantir que les parcours utilisateur critiques fonctionnent correctement via des tests E2E automatisés avec Playwright sur Web/PWA.
Principe fondamental¶
Test comme un utilisateur réel : Interactions naturelles (clic, saisie, navigation), attentes appropriées, isolation des tests.
Installation & Configuration¶
Installation¶
npm install --save-dev @playwright/test
npx playwright install # Installe browsers (Chromium, Firefox, WebKit)
Configuration (playwright.config.ts)¶
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Mobile viewports
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Structure de tests¶
Organisation fichiers¶
e2e/
├── auth/
│ ├── login.spec.ts
│ ├── register.spec.ts
│ └── biometric.spec.ts
├── documents/
│ ├── upload.spec.ts
│ ├── download.spec.ts
│ ├── share.spec.ts
│ └── certify.spec.ts
├── fixtures/
│ └── test-data.ts
├── helpers/
│ └── auth-helper.ts
└── pages/ # Page Object Model
├── login-page.ts
├── dashboard-page.ts
└── document-page.ts
Pattern Page Object Model (POM)¶
Exemple : LoginPage¶
// e2e/pages/login-page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.loginButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('[role="alert"]');
}
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 expectError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
Utilisation dans test¶
// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login-page';
test.describe('TC-AUTH: User Authentication', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('TC-AUTH-01: should login with valid credentials', async ({ page }) => {
await loginPage.login('user@example.com', 'ValidPassword123!');
// Vérification redirection dashboard
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('TC-AUTH-02: should show error with invalid credentials', async () => {
await loginPage.login('user@example.com', 'WrongPassword');
await loginPage.expectError('Invalid email or password');
// Reste sur page login
await expect(loginPage.page).toHaveURL('/login');
});
});
Sélecteurs robustes¶
Priorités (du meilleur au pire)¶
- data-testid (recommandé)
- role + name (accessible)
- label (formulaires)
- placeholder (formulaires)
- text content (fragile)
- CSS classes (très fragile)
Exemples¶
// ✅ EXCELLENT - data-testid
page.locator('[data-testid="upload-button"]')
// ✅ BON - role + name (accessibilité)
page.getByRole('button', { name: 'Upload Document' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('link', { name: 'Sign Out' })
// ✅ BON - label (formulaires)
page.getByLabel('Email')
page.getByLabel('Password')
// ⚠️ ACCEPTABLE - placeholder
page.getByPlaceholder('Enter your email')
// ⚠️ ACCEPTABLE - text
page.getByText('Welcome back')
// ❌ FRAGILE - CSS classes
page.locator('.btn-primary') // Peut casser au refactoring CSS
Ajout data-testid dans React¶
// ✅ Ajouter data-testid pour tests
<button
data-testid="upload-document-button"
onClick={handleUpload}
>
Upload Document
</button>
<input
data-testid="email-input"
type="email"
name="email"
/>
Attentes (Assertions)¶
Attentes automatiques¶
Playwright attend automatiquement (timeout: 30s par défaut) :
// ✅ Attend que l'élément soit visible
await expect(page.locator('#username')).toBeVisible();
// ✅ Attend que le texte apparaisse
await expect(page.locator('h1')).toHaveText('Dashboard');
// ✅ Attend que l'élément soit cliquable
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
// ✅ Attend la navigation
await page.click('a[href="/dashboard"]');
await expect(page).toHaveURL('/dashboard');
Attentes personnalisées¶
// Attendre un état spécifique
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// Attendre un sélecteur
await page.waitForSelector('[data-testid="document-list"]', {
state: 'visible',
timeout: 10000,
});
// Attendre une condition
await page.waitForFunction(() => {
return document.querySelectorAll('.document-item').length > 0;
});
Gestion de l'authentification¶
Pattern : Réutilisation state auth¶
// e2e/helpers/auth-helper.ts
import { Page } from '@playwright/test';
export async function authenticate(page: Page, credentials: {
email: string;
password: string;
}) {
await page.goto('/login');
await page.locator('input[name="email"]').fill(credentials.email);
await page.locator('input[name="password"]').fill(credentials.password);
await page.locator('button[type="submit"]').click();
// Attendre redirection
await page.waitForURL('/dashboard');
// Sauvegarder state auth
const storage = await page.context().storageState();
return storage;
}
// Utilisation dans tests
test.describe('Documents', () => {
test.use({
storageState: 'e2e/.auth/user.json', // Réutilise auth
});
test('TC-DOC-01: should upload document', async ({ page }) => {
// Déjà authentifié
await page.goto('/documents');
// ...
});
});
Setup global (global-setup.ts)¶
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import path from 'path';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
// Login
await page.goto('http://localhost:3000/login');
await page.locator('input[name="email"]').fill('test@example.com');
await page.locator('input[name="password"]').fill('TestPassword123!');
await page.locator('button[type="submit"]').click();
await page.waitForURL('http://localhost:3000/dashboard');
// Sauvegarder auth state
await page.context().storageState({
path: path.join(__dirname, 'e2e/.auth/user.json'),
});
await browser.close();
}
export default globalSetup;
Tests de fichiers (Upload)¶
test('TC-DOC-01: should upload encrypted document', async ({ page }) => {
await page.goto('/documents/upload');
// Upload fichier
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('e2e/fixtures/test-document.pdf');
// Attendre encryption client-side
await page.waitForSelector('[data-testid="encryption-progress"]');
await page.waitForSelector('[data-testid="encryption-complete"]');
// Submit
await page.getByRole('button', { name: 'Upload' }).click();
// Vérification succès
await expect(page.locator('[role="alert"]')).toContainText(
'Document uploaded successfully'
);
// Vérification apparition dans liste
await page.goto('/documents');
await expect(page.locator('[data-testid="document-list"]')).toContainText(
'test-document.pdf'
);
});
Tests de téléchargement¶
test('TC-DOC-02: should download encrypted document', async ({ page }) => {
await page.goto('/documents');
// Click download
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Download' }).first().click();
const download = await downloadPromise;
// Vérification nom fichier
expect(download.suggestedFilename()).toMatch(/\.pdf$/);
// Sauvegarder fichier
await download.saveAs(`e2e/downloads/${download.suggestedFilename()}`);
});
Tests multi-navigateurs¶
test.describe('Cross-browser compatibility', () => {
test('should work on Chromium', async ({ page, browserName }) => {
test.skip(browserName !== 'chromium', 'Chromium only');
// Test spécifique Chromium
});
test('should work on all browsers', async ({ page }) => {
// Test sur tous les navigateurs (Chromium, Firefox, WebKit)
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
});
Capture d'artefacts¶
Screenshots¶
test('TC-AUTH-01: login flow', async ({ page }) => {
await page.goto('/login');
// Screenshot avant action
await page.screenshot({
path: 'e2e/screenshots/login-before.png',
fullPage: true,
});
await page.locator('input[name="email"]').fill('user@example.com');
await page.locator('input[name="password"]').fill('password');
await page.locator('button[type="submit"]').click();
// Screenshot après action
await page.screenshot({
path: 'e2e/screenshots/login-after.png',
fullPage: true,
});
});
Vidéos¶
// playwright.config.ts
use: {
video: 'retain-on-failure', // Vidéo seulement si échec
// ou
video: 'on', // Vidéo pour tous les tests
}
Traces¶
// playwright.config.ts
use: {
trace: 'on-first-retry', // Trace seulement si retry
// ou
trace: 'on', // Trace pour tous les tests
}
// Voir la trace
// npx playwright show-trace trace.zip
Isolation des tests¶
Règles impératives¶
- Chaque test est indépendant : Aucun test ne dépend d'un autre
- State propre : Chaque test commence avec un state vierge
- Pas d'ordre : Tests exécutables dans n'importe quel ordre
- Cleanup automatique : Browser context recréé entre tests
// ✅ BON - Isolation complète
test.describe('Documents', () => {
test.beforeEach(async ({ page }) => {
// Setup avant CHAQUE test
await page.goto('/documents');
});
test('TC-DOC-01: upload', async ({ page }) => {
// Test isolé
});
test('TC-DOC-02: download', async ({ page }) => {
// Test isolé, ne dépend pas de TC-DOC-01
});
});
// ❌ MAUVAIS - Dépendance entre tests
test.describe('Documents', () => {
let documentId: string;
test('upload', async ({ page }) => {
// ...
documentId = await getUploadedDocumentId(); // ❌ State partagé
});
test('download', async ({ page }) => {
await downloadDocument(documentId); // ❌ Dépend du test précédent
});
});
Parcours critiques ProbatioVault¶
TC-AUTH: Authentification¶
test('TC-AUTH-01: Register + Biometric + Login', async ({ page }) => {
// 1. Inscription
await page.goto('/register');
await page.getByLabel('Email').fill('newuser@example.com');
await page.getByLabel('Password').fill('SecurePass123!');
await page.getByRole('button', { name: 'Register' }).click();
// 2. Configuration biométrie (simulé)
await expect(page).toHaveURL('/setup-biometric');
await page.getByRole('button', { name: 'Enable Face ID' }).click();
// 3. Logout
await page.getByRole('button', { name: 'Sign Out' }).click();
// 4. Login avec biométrie
await page.goto('/login');
await page.getByLabel('Email').fill('newuser@example.com');
await page.getByRole('button', { name: 'Sign In with Biometric' }).click();
// Vérification succès
await expect(page).toHaveURL('/dashboard');
});
TC-DOC: Documents¶
test('TC-DOC-01: Full document lifecycle', async ({ page }) => {
await page.goto('/documents');
// 1. Upload
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles('e2e/fixtures/contract.pdf');
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.locator('[role="alert"]')).toContainText('uploaded');
// 2. Visualisation
await page.getByText('contract.pdf').click();
await expect(page.locator('iframe')).toBeVisible(); // PDF viewer
// 3. Partage (PRE)
await page.getByRole('button', { name: 'Share' }).click();
await page.getByLabel('Recipient email').fill('recipient@example.com');
await page.getByRole('button', { name: 'Share Document' }).click();
await expect(page.locator('[role="alert"]')).toContainText('shared');
// 4. Certification
await page.getByRole('button', { name: 'Certify' }).click();
await expect(page.locator('[data-testid="certified-badge"]')).toBeVisible();
});
Exécution tests¶
Commandes¶
# Tous les tests
npx playwright test
# Mode headed (voir browser)
npx playwright test --headed
# Mode debug
npx playwright test --debug
# Tests spécifiques
npx playwright test auth/
npx playwright test login.spec.ts
# Navigateur spécifique
npx playwright test --project=chromium
# Tests en parallèle
npx playwright test --workers=4
# Rapport HTML
npx playwright show-report
Checklist Playwright¶
Avant commit¶
- Tests utilisent Page Object Model
- Sélecteurs robustes (data-testid ou role)
- Attentes automatiques (expect)
- Tests isolés (aucune dépendance)
- Screenshots sur échec activés
- Vidéos sur échec activées
- Tous les tests passent (0 failures)
Escalade¶
Escalader vers Agent QA (coordinateur) si : - Tests E2E échouent de manière intermittente (flaky) - Impossibilité de sélectionner éléments (sélecteurs fragiles) - Performance tests trop lente (> 30s par test) - Besoin de tests cross-origin (limitations CORS)
Références¶
- Playwright Docs: https://playwright.dev/
- Best Practices: https://playwright.dev/docs/best-practices
- Page Object Model: https://playwright.dev/docs/pom
- Selectors: https://playwright.dev/docs/selectors
Historique¶
| Version | Date | Changement |
|---|---|---|
| 1.0.0 | 2026-01-14 | Création initiale |