storageState de Playwright sauvegarde les cookies et le localStorage après une seule connexion et réutilise cet état dans tous les tests suivants. Chaque test démarre pré-authentifié, sans les 2 à 4 secondes d'overhead de connexion UI.
Ce que storageState sauvegarde réellement
Quand vous vous authentifiez dans un navigateur, le serveur prouve votre identité via l'un de deux mécanismes. Soit un cookie (généralement un ID de session ou un JWT dans un cookie HTTP-only), soit un token stocké dans localStorage ou sessionStorage. Parfois les deux.
storageState de Playwright capture tout ça. Appeler context.storageState() retourne un objet JSON contenant chaque cookie du contexte et un snapshot de localStorage et sessionStorage pour chaque origine. Vous pouvez écrire ce JSON sur disque, et quand Playwright crée un nouveau contexte de navigateur avec storageState: './auth.json', il pré-charge toutes ces données avant la première navigation. Pour le serveur, la requête est identique à celle provenant de la session authentifiée d'origine.
// À quoi ressemble le fichier sauvegardé (abrégé)
{
"cookies": [
{
"name": "session",
"value": "eyJhbGciOi...",
"domain": "lab.becomeqa.com",
"path": "/",
"expires": 1748000000,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
}
],
"origins": [
{
"origin": "https://lab.becomeqa.com",
"localStorage": [
{ "name": "auth_token", "value": "eyJhbGciOi..." }
]
}
]
}Le fichier n'est que du JSON. Vous pouvez l'inspecter, le committer dans une branche de test uniquement, ou le régénérer à la demande. La plupart des équipes l'ajoutent au .gitignore et le régénèrent au début de chaque exécution CI.
Configurer global-setup.ts
Le schéma standard est un fichier global-setup.ts qui s'exécute une fois avant toute la suite de tests. Il lance un navigateur, fait la vraie connexion UI, et sauvegarde l'état résultant dans un fichier. Chaque worker de test lit ensuite ce fichier au lieu de se connecter.
// global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://lab.becomeqa.com');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
// Attendre d'être réellement sur le tableau de bord authentifié
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
// Sauvegarder cookies + localStorage dans un fichier
await context.storageState({ path: 'playwright/.auth/admin.json' });
await browser.close();
}
export default globalSetup;Créez le répertoire avant de lancer les tests, sinon Playwright lancera une erreur de fichier introuvable :
mkdir -p playwright/.authAjoutez le répertoire au .gitignore pour que les tokens d'authentification ne se retrouvent pas dans le contrôle de version :
# .gitignore
playwright/.auth/Connecter ça dans playwright.config.ts
Deux choses doivent se produire dans votre config. D'abord, indiquer à Playwright où se trouve global-setup.ts. Ensuite, indiquer à chaque projet de test d'utiliser l'état sauvegardé comme contexte de départ.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
globalSetup: require.resolve('./global-setup'),
use: {
baseURL: 'https://lab.becomeqa.com',
storageState: 'playwright/.auth/admin.json',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
});C'est suffisant pour éliminer la connexion de chaque test. Le storageState défini dans use s'applique globalement, donc chaque contexte de navigateur créé par Playwright démarrera pré-authentifié.
const ADMIN_AUTH = 'playwright/.auth/admin.json'.Le schéma de projet setup (recommandé pour les grandes suites)
Le hook globalSetup fonctionne, mais a un inconvénient : il s'exécute en dehors du système de projets et de reporters de Playwright. Les échecs dans global-setup.ts produisent une sortie minimale, et la configuration n'apparaît pas dans votre rapport HTML.
L'alternative recommandée, introduite dans Playwright 1.31, est un projet setup dédié. Il s'exécute avant les autres projets et bénéficie du pipeline de reporting complet.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
// Le projet setup s'exécute en premier et produit les fichiers d'auth
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Les projets de test dépendent de la fin de setup
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['setup'],
},
],
});Le fichier de setup lui-même est un fichier de test Playwright ordinaire :
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('s\'authentifier en tant qu\'admin', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});L'étape de connexion apparaît maintenant dans votre rapport HTML, la logique de relance s'applique si la page de connexion est instable. Les captures d'écran en cas d'échec sont capturées automatiquement.
storageState par projet pour plusieurs rôles utilisateur
La plupart des applications ont plus d'un rôle utilisateur, et vous devez les tester indépendamment. Un admin voit les contrôles de gestion. Un utilisateur ordinaire non. Si vous exécutez des tests admin avec la session d'un utilisateur ordinaire, ils échoueront pour la mauvaise raison.
Ajoutez une étape de setup par rôle, un fichier d'auth par rôle, et un projet de test par rôle :
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('s\'authentifier en tant qu\'admin', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/admin.json' });
});
setup('s\'authentifier en tant qu\'utilisateur ordinaire', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('user@becomeqa.com');
await page.getByLabel('Password').fill('userpass456');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
const ADMIN_AUTH = 'playwright/.auth/admin.json';
const USER_AUTH = 'playwright/.auth/user.json';
export default defineConfig({
use: {
baseURL: 'https://lab.becomeqa.com',
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Tests admin
{
name: 'admin-chromium',
testMatch: /.*admin.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: ADMIN_AUTH,
},
dependencies: ['setup'],
},
// Tests utilisateur ordinaire
{
name: 'user-chromium',
testMatch: /.*user.*\.spec\.ts/,
use: {
...devices['Desktop Chrome'],
storageState: USER_AUTH,
},
dependencies: ['setup'],
},
// Tests sans authentification (page d'accueil, tests du flux de connexion)
{
name: 'public',
testMatch: /.*public.*\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
},
],
});Chaque projet charge un fichier d'auth différent, et vos tests admin ne tournent jamais accidentellement avec la session d'un utilisateur ordinaire.
public et utiliser la fixture page brute sans aucun storageState. Le but même de ces tests est de passer par l'UI de connexion.Fixtures avec portée worker pour storageState (schéma avancé)
storageState dans playwright.config.ts a un comportement subtil : il s'applique au contexte du navigateur. Si un test modifie l'état d'authentification, comme changer l'email ou se déconnecter, le contexte modifié peut se propager au test suivant dans le même worker.
La solution est de créer un nouveau contexte par test, chargé depuis le fichier d'auth statique, plutôt que de partager un contexte entre les tests. Une fixture avec portée worker gère ça proprement :
// fixtures/auth.fixture.ts
import { test as base, BrowserContext } from '@playwright/test';
import path from 'path';
const ADMIN_AUTH = path.resolve('playwright/.auth/admin.json');
type AuthFixtures = {
// Portée worker : le chemin du fichier d'auth, chargé une fois par worker
adminStorageState: string;
};
type TestFixtures = {
// Portée test : un nouveau contexte par test, chargé depuis le fichier d'état
adminContext: BrowserContext;
};
export const test = base.extend<TestFixtures, AuthFixtures>({
// La fixture worker détient juste le chemin, valide que le fichier existe une fois
adminStorageState: [
async ({}, use) => {
await use(ADMIN_AUTH);
},
{ scope: 'worker' },
],
// La fixture test crée un nouveau contexte depuis l'état sauvegardé
adminContext: async ({ browser, adminStorageState }, use) => {
const context = await browser.newContext({
storageState: adminStorageState,
});
await use(context);
await context.close();
},
});
export { expect } from '@playwright/test';Les tests qui utilisent cette fixture obtiennent un contexte de navigateur isolé qui démarre authentifié, mais les changements d'état dans le test n'affectent pas les autres tests :
// tests/admin-items.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
test('l\'admin peut voir le panneau de gestion', async ({ adminContext }) => {
const page = await adminContext.newPage();
await page.goto('/');
await expect(page.getByRole('link', { name: 'Admin Panel' })).toBeVisible();
});
test('l\'admin peut supprimer n\'importe quel item', async ({ adminContext }) => {
const page = await adminContext.newPage();
await page.goto('/items');
await page.getByTestId('item-row-1').getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByTestId('item-row-1')).not.toBeVisible();
});Chaque test obtient son propre BrowserContext fraîchement initialisé depuis le fichier d'auth. La suppression dans le deuxième test n'affecte aucun état partagé.
Combiner storageState avec une connexion via API (setup plus rapide)
Le fichier auth.setup.ts montré plus haut fait une connexion UI complète : il navigue, clique, remplit des formulaires, et attend. Ça fonctionne, mais c'est quand même plusieurs secondes. Sur une machine CI lente ou quand le formulaire de connexion a des animations, ça peut devenir le goulot d'étranglement.
Si votre application a un endpoint d'API de connexion, vous pouvez l'appeler directement depuis l'étape de setup, ignorer l'UI entièrement, et écrire le token résultant dans l'état de stockage manuellement. C'est typiquement 5 à 10 fois plus rapide que l'approche UI :
// tests/auth.setup.ts (version via API)
import { test as setup, request } from '@playwright/test';
import fs from 'fs';
import path from 'path';
const AUTH_FILE = 'playwright/.auth/admin.json';
setup('s\'authentifier en tant qu\'admin via API', async ({ request }) => {
// Appeler l'endpoint de connexion directement
const response = await request.post('https://lab.becomeqa.com/api/auth/login', {
data: {
email: 'admin@becomeqa.com',
password: 'testpass123',
},
});
const { token, sessionCookie } = await response.json();
// Construire manuellement la structure storageState
const storageState = {
cookies: [
{
name: 'session',
value: sessionCookie,
domain: 'lab.becomeqa.com',
path: '/',
expires: Math.floor(Date.now() / 1000) + 86400, // 24 heures
httpOnly: true,
secure: true,
sameSite: 'Lax' as const,
},
],
origins: [
{
origin: 'https://lab.becomeqa.com',
localStorage: [
{ name: 'auth_token', value: token },
],
},
],
};
// S'assurer que le répertoire existe
fs.mkdirSync(path.dirname(AUTH_FILE), { recursive: true });
fs.writeFileSync(AUTH_FILE, JSON.stringify(storageState, null, 2));
});Le compromis : cette approche nécessite de connaître la forme exacte du stockage d'authentification de votre application (quels cookies elle définit, quelles clés localStorage elle lit). L'approche UI fonctionne quelle que soit l'implémentation. Vous vous connectez et sauvegardez ce que le navigateur obtient. Commencez par l'approche UI, passez à l'API si la connexion devient un vrai goulot d'étranglement mesurable.
Quand storageState casse
storageState n'est pas magique. C'est un snapshot de l'état du navigateur à un moment donné. Quelques situations le feront cesser de fonctionner.
Expiration du token. Si votre application utilise des JWT à courte durée de vie (15 minutes, 1 heure), le token sauvegardé sera expiré quand les tests suivants s'exécuteront. La solution est de régénérer le fichier d'auth au début de chaque exécution CI (ce que vous devriez faire de toute façon). Autrement, passez à une connexion via API qui émet toujours un token frais.
Invalidation de session côté serveur. Certaines applications invalident les sessions quand elles détectent des patterns anormaux. Plusieurs requêtes simultanées depuis la "même" session à travers différents processus workers est un tel pattern. Si vous voyez des erreurs 401 aléatoires dans des tests qui devraient être authentifiés, vérifiez les protections contre la fixation de session. Certaines applications traitent les workers de test parallèles comme suspects.
Authentification à deux facteurs. Le 2FA casse entièrement le setup storageState basé sur l'UI. Le flux de connexion nécessite un code TOTP ou une vérification SMS que vous ne pouvez pas automatiser via Playwright de façon générale. Les solutions pratiques : utiliser un compte de test dédié avec le 2FA désactivé (si votre application le permet), ou une connexion via API qui émet des tokens sans 2FA. On peut aussi ajouter une variable d'environnement qui contourne le 2FA quand NODE_ENV=test.
Sessions liées au navigateur. Certaines applications lient les sessions à des empreintes de navigateur, des certificats client TLS, ou des identifiants d'appareil. Si vos cookies de session ont des attributs qui les restreignent à des caractéristiques d'appareil spécifiques, les sauvegarder et les restaurer sur des instances de navigateur différentes ne fonctionnera pas. C'est rare dans les applications web mais vaut la peine de le savoir.
// Vérifier que l'état sauvegardé est toujours valide. Ajoutez cette vérification à auth.setup.ts.
setup('s\'authentifier en tant qu\'admin', async ({ page }) => {
const AUTH_FILE = 'playwright/.auth/admin.json';
if (fs.existsSync(AUTH_FILE)) {
// Vérifier si le token existant est toujours valide
const checkContext = await browser.newContext({ storageState: AUTH_FILE });
const checkPage = await checkContext.newPage();
await checkPage.goto('/');
const isAuthenticated = await checkPage.getByText('My Travel Items').isVisible();
await checkContext.close();
if (isAuthenticated) {
console.log('L\'état d\'auth existant est valide, connexion ignorée');
return; // Réutiliser le fichier existant
}
}
// L'état est invalide ou manquant, faire la connexion complète
await page.goto('/');
await page.getByRole('button', { name: 'Login' }).click();
await page.getByLabel('Username').fill('admin@becomeqa.com');
await page.getByLabel('Password').fill('testpass123');
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('My Travel Items').waitFor({ state: 'visible' });
await page.context().storageState({ path: AUTH_FILE });
});playwright/.auth/*.json contient de vrais tokens de session qui donnent accès à vos comptes de test. Ajoutez le répertoire au .gitignore et faites tourner régulièrement les mots de passe des comptes de test. Si vous utilisez des variables d'environnement pour les credentials (ce que vous devriez faire en CI), assurez-vous que ces variables ne sont pas loggées dans la sortie de votre pipeline.storageState est le changement avec le meilleur retour sur investissement que vous pouvez faire sur une suite Playwright lente. La configuration prend environ 30 minutes et peut réduire votre temps de test total de 20 à 30% sur les suites où la plupart des tests nécessitent une authentification.
→ See also: Fixtures Personnalisés dans Playwright: Le Modèle qui Rend les Tests Lisibles | Fichier de Configuration Playwright Expliqué: Toutes les Options à Connaître | Tests d'API avec l'APIRequestContext de Playwright (Sans Postman) | Isolation des Tests: Pourquoi Chaque Test Playwright Doit Être sans État | Configuration et Nettoyage Global dans Playwright