page.route() de Playwright intercepte toute requête faite par votre application et vous permet de retourner une réponse mockée, modifier la requête ou l'annuler entièrement. Vous pouvez tester les états de chargement, les réponses d'erreur et les conditions réseau lentes sans backend en production.

Pourquoi intercepter les requêtes réseau ?

Tester des états difficiles à reproduire : que montre l'UI quand le serveur retourne 500 ? Y a-t-il un spinner quand l'API prend 10 secondes ? Le checkout affiche-t-il une erreur utile quand la passerelle de paiement est hors ligne ? Rendre les tests plus rapides et plus fiables : mocker le backend pour ne pas dépendre de données réelles, bloquer les analytics, pubs et scripts de tracking qui ralentissent le chargement, et éviter les limites de débit sur les APIs externes. Tester sans backend : développer des tests UI avant que l'API soit construite, et tester des cas limites difficiles à déclencher dans un vrai système.

Mocking de routes de base

page.route() intercepte les requêtes correspondant à un schéma d'URL :

test('affiche l\'état de chargement', async ({ page }) => {
  // Intercepter et retarder l'API des produits
  await page.route('/api/products', async (route) => {
    await new Promise(resolve => setTimeout(resolve, 3000));  // 3 secondes de délai
    await route.continue();  // Laisser passer la vraie requête
  });
  
  await page.goto('/products');
  
  // Le spinner de chargement doit être visible pendant le délai
  await expect(page.getByTestId('loading-spinner')).toBeVisible();
  
  // Attendre le chargement des produits
  await expect(page.getByTestId('product-card').first()).toBeVisible();
});

Retourner des réponses mockées

Au lieu de transmettre au vrai serveur, retournez des données fictives :

const mockUsers = [
  { id: 1, name: 'Alice', email: 'alice@test.com', role: 'admin' },
  { id: 2, name: 'Bob', email: 'bob@test.com', role: 'member' },
];

test('le tableau d\'utilisateurs affiche tous les utilisateurs', async ({ page }) => {
  await page.route('/api/users', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify(mockUsers),
    });
  });
  
  await page.goto('/admin/users');
  
  const rows = page.getByTestId('user-row');
  await expect(rows).toHaveCount(2);
  await expect(rows.first()).toContainText('Alice');
  await expect(rows.last()).toContainText('Bob');
});

Tester les états d'erreur

test('affiche l\'erreur quand l\'API échoue', async ({ page }) => {
  await page.route('/api/products', async (route) => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' }),
    });
  });
  
  await page.goto('/products');
  
  await expect(page.getByTestId('error-message')).toBeVisible();
  await expect(page.getByTestId('error-message')).toContainText('Une erreur est survenue');
  await expect(page.getByTestId('retry-button')).toBeVisible();
});

test('affiche l\'état vide quand pas de produits', async ({ page }) => {
  await page.route('/api/products', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([]),
    });
  });
  
  await page.goto('/products');
  
  await expect(page.getByTestId('empty-state')).toBeVisible();
  await expect(page.getByTestId('empty-state')).toContainText('Aucun produit trouvé');
});

test('affiche le message d\'erreur réseau', async ({ page }) => {
  await page.route('/api/products', async (route) => {
    await route.abort('failed');  // Simuler une panne réseau
  });
  
  await page.goto('/products');
  
  await expect(page.getByTestId('network-error')).toBeVisible();
});

Schémas d'URL

page.route() supporte les schémas glob et les regex :

// URL exacte
await page.route('/api/users', handler);

// Joker
await page.route('/api/users/*', handler);  // /api/users/1, /api/users/abc
await page.route('/api/**', handler);        // Toutes les routes API

// Regex
await page.route(/\/api\/users\/\d+/, handler);  // /api/users/123

// Glob avec query string
await page.route('/api/products?*', handler);  // /api/products?page=1&limit=10

Intercepter et modifier les requêtes

Lisez la requête réelle avant de décider quoi faire :

test('utilise le bon en-tête d\'auth', async ({ page }) => {
  let capturedAuthHeader = '';
  
  await page.route('/api/users', async (route) => {
    capturedAuthHeader = route.request().headers()['authorization'] || '';
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify([]),
    });
  });
  
  await page.goto('/admin/users');
  
  expect(capturedAuthHeader).toMatch(/Bearer .+/);
});

test('envoie le bon corps de requête', async ({ page }) => {
  let capturedBody = '';
  
  await page.route('/api/auth/login', async (route) => {
    capturedBody = route.request().postData() || '';
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify({ token: 'fake-token', user: { id: 1 } }),
    });
  });
  
  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"]');
  
  const body = JSON.parse(capturedBody);
  expect(body.email).toBe('user@test.com');
  expect(body.password).toBe('ValidPass1');
});

Modifier les vraies réponses

Interceptez une vraie requête et modifiez sa réponse :

test('gère gracieusement des rôles utilisateur supplémentaires', async ({ page }) => {
  await page.route('/api/users/1', async (route) => {
    // Laisser passer la vraie requête
    const response = await route.fetch();
    const body = await response.json();
    
    // Modifier la réponse
    body.role = 'super-admin';  // Ce rôle peut ne pas exister en base de test
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify(body),
    });
  });
  
  await page.goto('/users/1');
  
  // Tester comment l'UI gère des valeurs de rôle inattendues
  await expect(page.getByTestId('role-badge')).toBeVisible();
});

Bloquer les requêtes tierces

Accélérez les tests en bloquant les scripts de tracking, analytics et publicités :

test.beforeEach(async ({ page }) => {
  // Bloquer les scripts tiers courants
  await page.route('**/*.{png,jpg,jpeg,gif,svg,ico,woff,woff2}', route => route.abort());
  await page.route('**/google-analytics.com/**', route => route.abort());
  await page.route('**/googletagmanager.com/**', route => route.abort());
  await page.route('**/hotjar.com/**', route => route.abort());
  await page.route('**/intercom.io/**', route => route.abort());
  await page.route('**/sentry.io/**', route => route.abort());
});

Attention : bloquer les images peut affecter les tests visuels ou les assertions sensibles à la mise en page.

Utiliser page.waitForRequest et page.waitForResponse

Attendez qu'une activité réseau spécifique se produise :

test('la soumission du formulaire envoie les bonnes données', async ({ page }) => {
  await page.goto('/login');
  
  // Commencer à attendre la requête AVANT l'action qui la déclenche
  const requestPromise = page.waitForRequest(req => 
    req.url().includes('/api/auth/login') && req.method() === 'POST'
  );
  
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');
  
  const request = await requestPromise;
  const body = JSON.parse(request.postData() || '{}');
  
  expect(body.email).toBe('user@test.com');
});

test('la page se recharge après sauvegarde', async ({ page }) => {
  await page.goto('/profile');
  
  // Attendre l'appel API de sauvegarde
  const responsePromise = page.waitForResponse(resp => 
    resp.url().includes('/api/users') && resp.status() === 200
  );
  
  await page.fill('[data-testid="name"]', 'Nouveau nom');
  await page.click('[data-testid="save"]');
  
  const response = await responsePromise;
  const body = await response.json();
  expect(body.name).toBe('Nouveau nom');
});

Schémas de données mockées réalistes

// data/mocks/users.ts
export const mockUser = (overrides = {}) => ({
  id: Math.floor(Math.random() * 10000),
  email: `user_${Date.now()}@test.com`,
  name: 'Utilisateur test',
  role: 'member',
  createdAt: new Date().toISOString(),
  ...overrides,
});

export const mockPaginatedResponse = <T>(items: T[], page = 1, limit = 10) => ({
  data: items,
  page,
  limit,
  total: items.length,
  totalPages: Math.ceil(items.length / limit),
});

test('le tableau admin gère 100 utilisateurs', async ({ page }) => {
  const users = Array.from({ length: 100 }, (_, i) => 
    mockUser({ id: i + 1, name: `Utilisateur ${i + 1}` })
  );
  
  await page.route('/api/users', async (route) => {
    const url = new URL(route.request().url());
    const pageNum = parseInt(url.searchParams.get('page') || '1');
    const limit = parseInt(url.searchParams.get('limit') || '10');
    const start = (pageNum - 1) * limit;
    const pageUsers = users.slice(start, start + limit);
    
    await route.fulfill({
      status: 200,
      body: JSON.stringify(mockPaginatedResponse(pageUsers, pageNum, limit)),
    });
  });
  
  await page.goto('/admin/users');
  
  // Vérifier que la pagination affiche le bon total
  await expect(page.getByTestId('total-count')).toContainText('100');
  await expect(page.getByTestId('user-row')).toHaveCount(10);  // Première page
});

Récapitulatif

| Méthode | Ce qu'elle fait |

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

| page.route(url, handler) | Intercepter les requêtes correspondant à l'URL |

| route.fulfill({...}) | Retourner une réponse mockée |

| route.continue() | Laisser passer la vraie requête |

| route.abort('failed') | Simuler une panne réseau |

| route.fetch() | Faire la vraie requête, récupérer la réponse |

| page.waitForRequest(filter) | Attendre qu'une requête spécifique se produise |

| page.waitForResponse(filter) | Attendre une réponse spécifique |

La plupart des tests flaky le sont parce qu'ils frappent un backend dans un état imprévisible. L'interception réseau supprime cette variable pour tout test dont la fiabilité dépend de ce que le serveur retourne.

→ See also: Interception Réseau, Mocking et Stubbing dans Playwright | Tests d'API avec Playwright: Au-delà de l'Interface | Stratégies d'Attente dans Playwright: Plus de sleep()