Les fixtures Playwright sont des valeurs préparées automatiquement avant un test et nettoyées après. page, browser, context et request sont intégrées. Vous pouvez aussi créer des fixtures personnalisées qui injectent des pages connectées, des données de test ou des clients API dans les tests.

Qu'est-ce qu'une fixture ?

Une fixture est une valeur (ou un objet) que Playwright prépare avant l'exécution de votre test et nettoie après. C'est de l'injection de dépendances pour les tests.

Au lieu d'écrire ceci :

test('l\'utilisateur peut se connecter', async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
  
  // code du test
  
  await page.close();
  await context.close();
  await browser.close();
});

Vous écrivez ceci :

test('l\'utilisateur peut se connecter', async ({ page }) => {
  // page est prête à l'emploi — setup et teardown sont gérés
});

Playwright gère le cycle de vie. Vous obtenez une page propre pour chaque test, fermée automatiquement après.

Fixtures intégrées

Playwright fournit ces fixtures prêtes à l'emploi :

| Fixture | Type | Ce que c'est |

|---------|------|--------------|

| page | Page | Une nouvelle page de navigateur (onglet) pour chaque test |

| browser | Browser | L'instance du navigateur (partagée entre les tests d'un worker) |

| context | BrowserContext | Contexte de navigateur, comme une fenêtre incognito |

| browserName | string | Le navigateur courant : 'chromium', 'firefox', 'webkit' |

| request | APIRequestContext | Client HTTP pour les requêtes API |

page

La fixture la plus utilisée. Chaque test obtient sa propre page isolée, fermée automatiquement après.

test('la page se charge correctement', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await expect(page).toHaveTitle(/BecomeQA/);
});

context

Un contexte de navigateur fonctionne comme une fenêtre incognito, avec ses propres cookies, stockage et session. Pour plusieurs pages dans un même test, créez-les depuis le même contexte :

test('deux pages partagent la même session', async ({ context }) => {
  const page1 = await context.newPage();
  const page2 = await context.newPage();
  
  await page1.goto('/login');
  // Connexion sur page1
  
  // page2 voit aussi la session (même contexte = mêmes cookies)
  await page2.goto('/dashboard');
  await expect(page2.getByTestId('user-name')).toBeVisible();
});

browser

Vous n'avez généralement pas besoin de browser directement. Utilisez-le quand vous devez créer des contextes avec des paramètres spécifiques :

test('viewport mobile', async ({ browser }) => {
  const context = await browser.newContext({
    viewport: { width: 390, height: 844 },
    userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
  });
  const page = await context.newPage();
  await page.goto('/');
  // Test sur une page de taille mobile
  await context.close();
});

request

Effectue des requêtes HTTP sans navigateur. Utilisé pour les tests d'API et pour configurer des données de test via API avant les tests UI.

test('créer un utilisateur via API', async ({ request }) => {
  const response = await request.post('/api/users', {
    data: { email: 'nouveau@test.com', password: 'ValidPass1' },
  });
  expect(response.status()).toBe(201);
});

browserName

Utilisez-le pour ignorer conditionnellement des tests selon le navigateur :

test('téléchargement de fichier', async ({ page, browserName }) => {
  test.skip(browserName === 'firefox', 'API de téléchargement différente dans Firefox');
  // ...
});

Fixtures personnalisées

Le vrai intérêt des fixtures, c'est que vous pouvez créer les vôtres. Les fixtures personnalisées fonctionnent exactement comme les intégrées : déclarées une fois, utilisables partout par déstructuration.

Fixture simple : une page pré-naviguée

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

type MyFixtures = {
  loggedInPage: Page;
};

export const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    // SETUP
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'user@test.com');
    await page.fill('[data-testid="password"]', 'ValidPass1');
    await page.click('[data-testid="submit"]');
    await page.waitForURL('/dashboard');
    
    // Donner au test l'accès à la page
    await use(page);
    
    // TEARDOWN (s'exécute après le test)
    // Rien à faire ici — la page se ferme automatiquement
  },
});

export { expect };

// tests/dashboard.spec.ts
import { test, expect } from '../fixtures';  // importez VOTRE test, pas @playwright/test

test('le tableau de bord affiche le message de bienvenue', async ({ loggedInPage }) => {
  // Déjà connecté — loggedInPage EST la page, après connexion
  await expect(loggedInPage.getByTestId('welcome')).toBeVisible();
});

Fixture avec Page Object

Le schéma le plus courant : une fixture qui fournit une instance initialisée de classe page object.

// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';

type PageObjects = {
  loginPage: LoginPage;
  dashboardPage: DashboardPage;
};

export const test = base.extend<PageObjects>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  dashboardPage: async ({ page }, use) => {
    await use(new DashboardPage(page));
  },
});

// tests/login.spec.ts
import { test, expect } from '../fixtures';

test('connexion réussie', async ({ loginPage, dashboardPage }) => {
  await loginPage.goto();
  await loginPage.login('user@test.com', 'ValidPass1');
  await expect(dashboardPage.welcomeMessage).toBeVisible();
});

Plus besoin d'instancier les page objects dans chaque test.

Fixture personnalisée avec teardown

Si votre fixture crée quelque chose qui nécessite un nettoyage :

type TestFixtures = {
  testUser: { id: number; email: string; token: string };
};

export const test = base.extend<TestFixtures>({
  testUser: async ({ request }, use) => {
    // SETUP : créer un utilisateur
    const response = await request.post('/api/users', {
      data: {
        email: `test_${Date.now()}@example.com`,
        password: 'ValidPass1',
        role: 'member',
      },
    });
    const user = await response.json();
    
    // Connexion pour obtenir le token
    const loginResp = await request.post('/api/auth/login', {
      data: { email: user.email, password: 'ValidPass1' },
    });
    const { token } = await loginResp.json();
    
    // Donner accès au test
    await use({ id: user.id, email: user.email, token });
    
    // TEARDOWN : supprimer l'utilisateur
    await request.delete(`/api/users/${user.id}`, {
      headers: { Authorization: `Bearer ${adminToken}` },
    });
  },
});

test('l\'utilisateur peut mettre à jour son profil', async ({ page, testUser }) => {
  // testUser a id, email, token — unique et frais pour chaque test
  await page.goto(`/users/${testUser.id}`);
  // ...
  // Après le test, l'utilisateur est automatiquement supprimé
});

Scope des fixtures

Par défaut, les fixtures ont un scope 'test' et sont recréées pour chaque test. Vous pouvez passer au scope 'worker' pour les fixtures coûteuses et sûres à partager :

export const test = base.extend<{}, { sharedToken: string }>({
  sharedToken: [async ({ request }, use) => {
    // S'exécute une fois par worker, pas une fois par test
    const response = await request.post('/api/auth/login', {
      data: { email: 'admin@test.com', password: 'AdminPass1' },
    });
    const { token } = await response.json();
    await use(token);
  }, { scope: 'worker' }],
});

Utilisez le scope 'worker' pour les éléments coûteux à recréer (seeds de base de données, génération de fichiers), en lecture seule (tokens d'authentification que vous lisez sans modifier), et sûrs à partager (aucun état qu'un test pourrait corrompre pour un autre).

Composer les fixtures

Les fixtures personnalisées peuvent dépendre d'autres fixtures (y compris d'autres fixtures personnalisées) :

export const test = base.extend<{
  loginPage: LoginPage;
  authenticatedPage: Page;
}>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  
  // Cette fixture UTILISE loginPage
  authenticatedPage: async ({ page, loginPage }, use) => {
    await loginPage.goto();
    await loginPage.login('user@test.com', 'ValidPass1');
    await page.waitForURL('/dashboard');
    await use(page);
  },
});

Structure de projet propre

project/
├── fixtures/
│   └── index.ts        ← exporte votre test étendu + expect
├── pages/
│   ├── LoginPage.ts
│   └── DashboardPage.ts
└── tests/
    ├── login.spec.ts   ← importe depuis fixtures/index.ts
    └── dashboard.spec.ts

Tous les fichiers de test importent depuis fixtures/index.ts, pas directement depuis @playwright/test. Chaque test a ainsi accès automatiquement à toutes les fixtures personnalisées.

Récapitulatif

| | Intégrées | Personnalisées |

|-|-----------|----------------|

| Où définies | Internals Playwright | test.extend() dans votre code |

| Où utilisées | Tout test avec { page }, { request }, etc. | Tout test utilisant votre test exporté |

| Exemples | page, browser, request | loginPage, testUser, authToken |

| Cycle de vie | Géré par Playwright | Vous définissez setup + await use() + teardown |

Les fixtures sont la façon la plus propre de partager la logique de setup entre les tests. Une fois que vous les utilisez pour les page objects et les états authentifiés, les tests deviennent bien plus courts et focalisés sur ce qu'ils testent réellement.

→ See also: Fixtures Personnalisés dans Playwright: Le Modèle qui Rend les Tests Lisibles | Structure des Tests Playwright: describe, beforeEach, afterEach et Hooks | Gérer l'Authentification dans Playwright avec storageState (Sans Se Connecter à Chaque Test)