Aller au contenu

Feuille de cheat de dramaturge

Playwright - Essais et automatisation Web modernes

Playwright est un cadre pour les tests Web et l'automatisation. Il permet de tester Chrome, Firefox et WebKit avec une seule API. Playwright est construit pour permettre l'automatisation web cross-browser qui est rapide, fiable et capable.

Copier toutes les commandes Générer PDF

Sommaire

  • [Installation] (#installation)
  • [Pour commencer] (#getting-started)
  • [Gestion des navigateurs] (#browser-management)
  • [Page Navigation] (#page-navigation)
  • [Interaction des éléments] (#element-interaction)
  • [Sélecteurs] (#selectors)
  • [Stratégies d'attente] (#waiting-strategies)
  • [Manipulation du formulaire] (#form-handling)
  • [Captures d'écran et vidéos] (#screenshots--videos)
  • [Interception du réseau] (#network-interception)
  • [Authentification] (#authentication)
  • [Essais mobiles] (#mobile-testing)
  • [Cadre d'essai] (#testing-framework)
  • [Débogage] (#debugging)
  • [Essais de performance] (#performance-testing)
  • [Essais API] (#api-testing)
  • [Essais visuels] (#visual-testing)
  • [CI/CD Intégration] (#cicd-integration)
  • [Caractéristiques avancées] (#advanced-features)
  • [Meilleures pratiques] (#best-practices)

Installation

Installation de base

# Install Playwright
npm init playwright@latest

# Or install manually
npm install -D @playwright/test

# Install browsers
npx playwright install

# Install specific browsers
npx playwright install chromium
npx playwright install firefox
npx playwright install webkit

Dépendances du système

# Install system dependencies (Linux)
npx playwright install-deps

# Ubuntu/Debian specific
sudo npx playwright install-deps

# Docker installation
FROM mcr.microsoft.com/playwright:v1.40.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
```_

### Configuration du projet
```bash
# Create new Playwright project
npm init playwright@latest my-tests

# Project structure
my-tests/
├── tests/
   └── example.spec.js
├── playwright.config.js
├── package.json
└── package-lock.json
```_

### Fichier de configuration
```javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Test directory
  testDir: './tests',

  // Run tests in files in parallel
  fullyParallel: true,

  // Fail the build on CI if you accidentally left test.only in the source code
  forbidOnly: !!process.env.CI,

  // Retry on CI only
  retries: process.env.CI ? 2 : 0,

  // Opt out of parallel tests on CI
  workers: process.env.CI ? 1 : undefined,

  // Reporter to use
  reporter: 'html',

  // Shared settings for all the projects below
  use: {
    // Base URL to use in actions like `await page.goto('/')`
    baseURL: 'http://127.0.0.1:3000',

    // Collect trace when retrying the failed test
    trace: 'on-first-retry',

    // Record video on failure
    video: 'retain-on-failure',

    // Take screenshot on failure
    screenshot: 'only-on-failure',
  },

  // Configure projects for major browsers
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],

  // Run your local dev server before starting the tests
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Commencer

Structure d'essai de base

// tests/example.spec.js
import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => {
  // Navigate to page
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring
  await expect(page).toHaveTitle(/Playwright/);

  // Click the get started link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects the URL to contain intro
  await expect(page).toHaveURL(/.*intro/);
});

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Click the get started link
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects the URL to contain intro
  await expect(page).toHaveURL(/.*intro/);
});

Essais de course

# Run all tests
npx playwright test

# Run tests in headed mode
npx playwright test --headed

# Run tests in specific browser
npx playwright test --project=chromium

# Run specific test file
npx playwright test tests/example.spec.js

# Run tests matching pattern
npx playwright test --grep "login"

# Run tests in debug mode
npx playwright test --debug

# Run tests with UI mode
npx playwright test --ui

Crochets d'essai

import { test, expect } from '@playwright/test';

test.describe('User Authentication', () => {
  test.beforeEach(async ({ page }) => {
    // Go to the starting url before each test
    await page.goto('https://example.com/login');
  });

  test.afterEach(async ({ page }) => {
    // Clean up after each test
    await page.evaluate(() => localStorage.clear());
  });

  test('should login successfully', async ({ page }) => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login-button');

    await expect(page).toHaveURL(/dashboard/);
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('#username', 'invalid');
    await page.fill('#password', 'wrong');
    await page.click('#login-button');

    await expect(page.locator('.error-message')).toBeVisible();
  });
});

Gestion du navigateur

Contextes du navigateur

import { test, expect, chromium } from '@playwright/test';

test('multiple contexts', async () => {
  // Launch browser
  const browser = await chromium.launch();

  // Create contexts
  const context1 = await browser.newContext();
  const context2 = await browser.newContext();

  // Create pages in different contexts
  const page1 = await context1.newPage();
  const page2 = await context2.newPage();

  // Navigate pages
  await page1.goto('https://example.com');
  await page2.goto('https://playwright.dev');

  // Contexts are isolated
  await page1.evaluate(() => localStorage.setItem('user', 'context1'));
  await page2.evaluate(() => localStorage.setItem('user', 'context2'));

  // Verify isolation
  const storage1 = await page1.evaluate(() => localStorage.getItem('user'));
  const storage2 = await page2.evaluate(() => localStorage.getItem('user'));

  expect(storage1).toBe('context1');
  expect(storage2).toBe('context2');

  // Cleanup
  await browser.close();
});

Options de lancement du navigateur

import { chromium, firefox, webkit } from '@playwright/test';

test('browser launch options', async () => {
  // Chromium with custom options
  const browser = await chromium.launch({
    headless: false,
    slowMo: 1000,
    devtools: true,
    args: [
      '--start-maximized',
      '--disable-web-security',
      '--disable-features=VizDisplayCompositor'
    ]
  });

  // Context with custom options
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    userAgent: 'Custom User Agent',
    locale: 'en-US',
    timezoneId: 'America/New_York',
    permissions: ['geolocation'],
    geolocation: { latitude: 40.7128, longitude: -74.0060 },
    colorScheme: 'dark',
    reducedMotion: 'reduce'
  });

  const page = await context.newPage();
  await page.goto('https://example.com');

  await browser.close();
});
import { test, chromium, firefox, webkit } from '@playwright/test';

test('cross-browser testing', async () => {
  const browsers = [
    { name: 'Chromium', browser: chromium },
    { name: 'Firefox', browser: firefox },
    { name: 'WebKit', browser: webkit }
  ];

  for (const { name, browser } of browsers) {
    console.log(`Testing with ${name}`);

    const browserInstance = await browser.launch();
    const context = await browserInstance.newContext();
    const page = await context.newPage();

    await page.goto('https://example.com');
    await expect(page).toHaveTitle(/Example/);

    await browserInstance.close();
  }
});
test('navigation', async ({ page }) => {
  // Navigate to URL
  await page.goto('https://example.com');

  // Navigate with options
  await page.goto('https://example.com', {
    waitUntil: 'networkidle',
    timeout: 30000
  });

  // Go back
  await page.goBack();

  // Go forward
  await page.goForward();

  // Reload page
  await page.reload();

  // Navigate and wait for specific event
  await Promise.all([
    page.waitForNavigation(),
    page.click('a[href="/next-page"]')
  ]);
});

Stratégies d'attente

test('wait strategies', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for load event
  await page.goto('https://example.com', { waitUntil: 'load' });

  // Wait for DOM content loaded
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

  // Wait for network idle
  await page.goto('https://example.com', { waitUntil: 'networkidle' });

  // Wait for specific element
  await page.waitForSelector('#content');

  // Wait for element to be visible
  await page.waitForSelector('#modal', { state: 'visible' });

  // Wait for element to be hidden
  await page.waitForSelector('#loading', { state: 'hidden' });

  // Wait for function to return true
  await page.waitForFunction(() => window.jQuery !== undefined);

  // Wait for response
  await page.waitForResponse('**/api/data');

  // Wait for request
  await page.waitForRequest('**/api/submit');
});

URL et Assertions de titre

test('URL and title checks', async ({ page }) => {
  await page.goto('https://example.com');

  // Check current URL
  expect(page.url()).toBe('https://example.com/');

  // Check URL contains
  expect(page.url()).toContain('example.com');

  // Check URL matches pattern
  await expect(page).toHaveURL(/example\.com/);

  // Check title
  await expect(page).toHaveTitle('Example Domain');

  // Check title contains
  await expect(page).toHaveTitle(/Example/);

  // Get title programmatically
  const title = await page.title();
  expect(title).toBe('Example Domain');
});

Interaction des éléments

Cliquer sur les éléments

test('clicking elements', async ({ page }) => {
  await page.goto('https://example.com');

  // Click by selector
  await page.click('#submit-button');

  // Click with options
  await page.click('#button', {
    button: 'right',
    clickCount: 2,
    delay: 1000,
    force: true,
    modifiers: ['Shift']
  });

  // Click at position
  await page.click('#element', { position: { x: 10, y: 10 } });

  // Double click
  await page.dblclick('#element');

  // Right click
  await page.click('#element', { button: 'right' });

  // Click and wait for navigation
  await Promise.all([
    page.waitForNavigation(),
    page.click('#link')
  ]);
});

Interactions de forme

test('form interactions', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Fill input
  await page.fill('#username', 'testuser');

  // Type with delay
  await page.type('#password', 'password123', { delay: 100 });

  // Clear input
  await page.fill('#field', '');

  // Select option
  await page.selectOption('#country', 'US');

  // Select multiple options
  await page.selectOption('#languages', ['en', 'es', 'fr']);

  // Check checkbox
  await page.check('#agree-terms');

  // Uncheck checkbox
  await page.uncheck('#newsletter');

  // Check radio button
  await page.check('#gender-male');

  // Upload file
  await page.setInputFiles('#file-upload', 'path/to/file.pdf');

  // Upload multiple files
  await page.setInputFiles('#files', ['file1.pdf', 'file2.jpg']);
});

Actions de clavier et de souris

test('keyboard and mouse actions', async ({ page }) => {
  await page.goto('https://example.com');

  // Keyboard actions
  await page.keyboard.press('Enter');
  await page.keyboard.press('Control+A');
  await page.keyboard.press('Meta+C'); // Cmd+C on Mac

  // Type text
  await page.keyboard.type('Hello World');

  // Key combinations
  await page.keyboard.down('Shift');
  await page.keyboard.press('ArrowLeft');
  await page.keyboard.up('Shift');

  // Mouse actions
  await page.mouse.move(100, 100);
  await page.mouse.click(100, 100);
  await page.mouse.dblclick(100, 100);

  // Drag and drop
  await page.dragAndDrop('#source', '#target');

  // Hover
  await page.hover('#menu-item');

  // Focus element
  await page.focus('#input-field');

  // Scroll
  await page.mouse.wheel(0, 100);
});

Sélecteurs

Sélecteurs CSS

test('CSS selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Basic selectors
  await page.click('#submit');           // ID
  await page.click('.button');           // Class
  await page.click('button');            // Tag
  await page.click('input[type="text"]'); // Attribute

  // Combinators
  await page.click('div > button');      // Child
  await page.click('div button');        // Descendant
  await page.click('div + button');      // Adjacent sibling
  await page.click('div ~ button');      // General sibling

  // Pseudo-selectors
  await page.click('button:first-child');
  await page.click('button:last-child');
  await page.click('button:nth-child(2)');
  await page.click('input:checked');
  await page.click('button:not(.disabled)');
});

Sélecteurs de texte

test('text selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Exact text match
  await page.click('text=Submit');

  // Partial text match
  await page.click('text=Sub');

  // Case insensitive
  await page.click('text=submit >> i');

  // Regular expression
  await page.click('text=/submit/i');

  // Text in specific element
  await page.click('button:has-text("Submit")');

  // Text with quotes
  await page.click('text="Click here"');
});

Sélecteurs basés sur les rôles

test('role-based selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // Button role
  await page.click('role=button[name="Submit"]');

  // Link role
  await page.click('role=link[name="Home"]');

  // Textbox role
  await page.fill('role=textbox[name="Username"]', 'user');

  // Checkbox role
  await page.check('role=checkbox[name="Agree"]');

  // Common roles
  await page.click('role=button');
  await page.click('role=link');
  await page.fill('role=textbox', 'text');
  await page.selectOption('role=combobox', 'option');
  await page.check('role=checkbox');
  await page.check('role=radio');
});

Localisateurs de dramaturges

test('playwright locators', async ({ page }) => {
  await page.goto('https://example.com');

  // Get by role
  await page.getByRole('button', { name: 'Submit' }).click();
  await page.getByRole('link', { name: 'Home' }).click();
  await page.getByRole('textbox', { name: 'Username' }).fill('user');

  // Get by text
  await page.getByText('Submit').click();
  await page.getByText(/submit/i).click();

  // Get by label
  await page.getByLabel('Username').fill('user');
  await page.getByLabel('Password').fill('pass');

  // Get by placeholder
  await page.getByPlaceholder('Enter username').fill('user');

  // Get by alt text
  await page.getByAltText('Profile picture').click();

  // Get by title
  await page.getByTitle('Close dialog').click();

  // Get by test id
  await page.getByTestId('submit-button').click();

  // Chaining locators
  const form = page.locator('#login-form');
  await form.getByLabel('Username').fill('user');
  await form.getByLabel('Password').fill('pass');
  await form.getByRole('button', { name: 'Login' }).click();
});

Sélecteurs avancés

test('advanced selectors', async ({ page }) => {
  await page.goto('https://example.com');

  // XPath
  await page.click('xpath=//button[@id="submit"]');

  // CSS with text
  await page.click('css=button >> text=Submit');

  // Multiple selectors (OR)
  await page.click('#submit, .submit-button, button[type="submit"]');

  // Selector with visibility
  await page.click('button:visible');

  // Selector with state
  await page.click('button:enabled');

  // Has selector
  await page.click('article:has(h2:text("Title"))');

  // Near selector
  await page.click('button:near(:text("Submit"))');

  // Nth match
  await page.click('button >> nth=1');

  // Filter by text
  await page.locator('button').filter({ hasText: 'Submit' }).click();

  // Filter by other locator
  await page.locator('article').filter({ has: page.locator('h2') }).first().click();
});

Stratégies d'attente

Élément en attente

test('element waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for element to be attached to DOM
  await page.waitForSelector('#element');

  // Wait for element to be visible
  await page.waitForSelector('#element', { state: 'visible' });

  // Wait for element to be hidden
  await page.waitForSelector('#element', { state: 'hidden' });

  // Wait for element to be detached from DOM
  await page.waitForSelector('#element', { state: 'detached' });

  // Wait with timeout
  await page.waitForSelector('#element', { timeout: 5000 });

  // Wait for multiple elements
  await Promise.all([
    page.waitForSelector('#element1'),
    page.waitForSelector('#element2'),
    page.waitForSelector('#element3')
  ]);
});

Auto-attente

test('auto-waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Playwright automatically waits for elements to be:
  // - Attached to DOM
  // - Visible
  // - Stable (not animating)
  // - Enabled
  // - Editable (for input actions)

  // These actions auto-wait
  await page.click('#button');           // Waits for button to be clickable
  await page.fill('#input', 'text');     // Waits for input to be editable
  await page.selectOption('#select', 'option'); // Waits for select to be enabled
  await page.check('#checkbox');         // Waits for checkbox to be checkable

  // Assertions also auto-wait
  await expect(page.locator('#element')).toBeVisible();
  await expect(page.locator('#input')).toHaveValue('expected');
  await expect(page.locator('#list li')).toHaveCount(5);
});

Attente personnalisée

test('custom waiting', async ({ page }) => {
  await page.goto('https://example.com');

  // Wait for function to return truthy value
  await page.waitForFunction(() => {
    return document.querySelector('#dynamic-content') !== null;
  });

  // Wait for function with arguments
  await page.waitForFunction(
    (selector) => document.querySelector(selector) !== null,
    '#dynamic-element'
  );

  // Wait for JavaScript condition
  await page.waitForFunction(() => window.dataLoaded === true);

  // Wait for network idle
  await page.waitForLoadState('networkidle');

  // Wait for DOM content loaded
  await page.waitForLoadState('domcontentloaded');

  // Wait for load event
  await page.waitForLoadState('load');

  // Wait for timeout
  await page.waitForTimeout(1000);

  // Wait for event
  await page.waitForEvent('dialog');
  await page.waitForEvent('download');
  await page.waitForEvent('popup');
});

Traitement des formulaires

Champs d'entrée

test('input field handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Text input
  await page.fill('#username', 'testuser');
  await page.fill('#email', 'test@example.com');

  // Password input
  await page.fill('#password', 'secretpassword');

  // Number input
  await page.fill('#age', '25');

  // Date input
  await page.fill('#birthdate', '2000-01-01');

  // Textarea
  await page.fill('#comments', 'This is a long comment text');

  // Clear field
  await page.fill('#field', '');

  // Type with delay (simulates human typing)
  await page.type('#search', 'search term', { delay: 100 });

  // Press keys
  await page.press('#input', 'Control+A');
  await page.press('#input', 'Backspace');
});

Sélectionnez les déroulants

test('select dropdown handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Select by value
  await page.selectOption('#country', 'US');

  // Select by label
  await page.selectOption('#country', { label: 'United States' });

  // Select by index
  await page.selectOption('#country', { index: 1 });

  // Select multiple options
  await page.selectOption('#languages', ['en', 'es', 'fr']);

  // Select all options
  const options = await page.locator('#languages option').allTextContents();
  await page.selectOption('#languages', options);

  // Get selected value
  const selectedValue = await page.inputValue('#country');
  expect(selectedValue).toBe('US');

  // Get all selected values
  const selectedValues = await page.evaluate(() => {
    const select = document.querySelector('#languages');
    return Array.from(select.selectedOptions).map(option => option.value);
  });
});

Boîtes à cocher et boutons radio

test('checkbox and radio handling', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Check checkbox
  await page.check('#agree-terms');
  await page.check('#newsletter');

  // Uncheck checkbox
  await page.uncheck('#newsletter');

  // Toggle checkbox
  const isChecked = await page.isChecked('#newsletter');
  if (!isChecked) {
    await page.check('#newsletter');
  }

  // Radio buttons
  await page.check('#gender-male');
  await page.check('#age-25-34');

  // Verify checkbox state
  expect(await page.isChecked('#agree-terms')).toBe(true);
  expect(await page.isChecked('#newsletter')).toBe(false);

  // Verify radio button state
  expect(await page.isChecked('#gender-male')).toBe(true);
  expect(await page.isChecked('#gender-female')).toBe(false);
});

Téléchargement de fichiers

test('file upload handling', async ({ page }) => {
  await page.goto('https://example.com/upload');

  // Single file upload
  await page.setInputFiles('#file-upload', 'tests/fixtures/document.pdf');

  // Multiple file upload
  await page.setInputFiles('#multiple-files', [
    'tests/fixtures/image1.jpg',
    'tests/fixtures/image2.png',
    'tests/fixtures/document.pdf'
  ]);

  // Upload from buffer
  await page.setInputFiles('#file-upload', {
    name: 'test.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('Hello World')
  });

  // Remove files
  await page.setInputFiles('#file-upload', []);

  // Verify file upload
  const fileName = await page.inputValue('#file-upload');
  expect(fileName).toContain('document.pdf');

  // Handle file chooser dialog
  const [fileChooser] = await Promise.all([
    page.waitForEvent('filechooser'),
    page.click('#upload-button')
  ]);

  await fileChooser.setFiles('tests/fixtures/document.pdf');
});

Présentation du formulaire

test('form submission', async ({ page }) => {
  await page.goto('https://example.com/form');

  // Fill form
  await page.fill('#username', 'testuser');
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'password123');
  await page.check('#agree-terms');

  // Submit form by clicking submit button
  await Promise.all([
    page.waitForNavigation(),
    page.click('#submit-button')
  ]);

  // Submit form by pressing Enter
  await Promise.all([
    page.waitForNavigation(),
    page.press('#username', 'Enter')
  ]);

  // Submit form programmatically
  await page.evaluate(() => {
    document.querySelector('#form').submit();
  });

  // Verify form submission
  await expect(page).toHaveURL(/success/);
  await expect(page.locator('.success-message')).toBeVisible();
});

Captures d'écran et vidéos

Captures d'écran

test('screenshots', async ({ page }) => {
  await page.goto('https://example.com');

  // Full page screenshot
  await page.screenshot({ path: 'screenshots/fullpage.png' });

  // Screenshot with options
  await page.screenshot({
    path: 'screenshots/page.png',
    fullPage: true,
    clip: { x: 0, y: 0, width: 800, height: 600 },
    quality: 80,
    type: 'jpeg'
  });

  // Element screenshot
  await page.locator('#header').screenshot({ path: 'screenshots/header.png' });

  // Screenshot to buffer
  const buffer = await page.screenshot();

  // Screenshot with mask
  await page.screenshot({
    path: 'screenshots/masked.png',
    mask: [page.locator('.sensitive-data')]
  });

  // Mobile screenshot
  await page.setViewportSize({ width: 375, height: 667 });
  await page.screenshot({ path: 'screenshots/mobile.png' });
});

Enregistrement vidéo

// playwright.config.js
export default defineConfig({
  use: {
    // Record video for all tests
    video: 'on',

    // Record video only on failure
    video: 'retain-on-failure',

    // Record video on first retry
    video: 'on-first-retry',
  },
});

test('video recording', async ({ page }) => {
  // Video is automatically recorded based on config
  await page.goto('https://example.com');
  await page.click('#button');
  await page.fill('#input', 'test');

  // Video will be saved to test-results folder
});

Comparaisons visuelles

test('visual comparisons', async ({ page }) => {
  await page.goto('https://example.com');

  // Compare full page
  await expect(page).toHaveScreenshot('homepage.png');

  // Compare element
  await expect(page.locator('#header')).toHaveScreenshot('header.png');

  // Compare with threshold
  await expect(page).toHaveScreenshot('page.png', { threshold: 0.2 });

  // Compare with mask
  await expect(page).toHaveScreenshot('page.png', {
    mask: [page.locator('.dynamic-content')]
  });

  // Update screenshots
  // npx playwright test --update-snapshots
});

Interception des réseaux

Demande d'interception

test('request interception', async ({ page }) => {
  // Intercept all requests
  await page.route('**/*', route => {
    console.log('Request:', route.request().url());
    route.continue();
  });

  // Intercept specific requests
  await page.route('**/api/users', route => {
    // Block request
    route.abort();

    // Modify request
    route.continue({
      headers: {
        ...route.request().headers(),
        'Authorization': 'Bearer token'
      }
    });

    // Mock response
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ users: [] })
    });
  });

  await page.goto('https://example.com');
});

Moqueur de réponse

test('response mocking', async ({ page }) => {
  // Mock API response
  await page.route('**/api/data', route => {
    route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        success: true,
        data: [
          { id: 1, name: 'Item 1' },
          { id: 2, name: 'Item 2' }
        ]
      })
    });
  });

  // Mock with file
  await page.route('**/api/users', route => {
    route.fulfill({
      path: 'tests/fixtures/users.json'
    });
  });

  // Mock error response
  await page.route('**/api/error', route => {
    route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: JSON.stringify({ error: 'Internal Server Error' })
    });
  });

  await page.goto('https://example.com');
});

Surveillance des réseaux

test('network monitoring', async ({ page }) => {
  const requests = [];
  const responses = [];

  // Monitor requests
  page.on('request', request => {
    requests.push({
      url: request.url(),
      method: request.method(),
      headers: request.headers(),
      postData: request.postData()
    });
  });

  // Monitor responses
  page.on('response', response => {
    responses.push({
      url: response.url(),
      status: response.status(),
      headers: response.headers()
    });
  });

  await page.goto('https://example.com');

  // Wait for specific request
  const [request] = await Promise.all([
    page.waitForRequest('**/api/data'),
    page.click('#load-data')
  ]);

  // Wait for specific response
  const [response] = await Promise.all([
    page.waitForResponse('**/api/submit'),
    page.click('#submit')
  ]);

  expect(response.status()).toBe(200);

  // Analyze network activity
  const apiRequests = requests.filter(req => req.url.includes('/api/'));
  expect(apiRequests).toHaveLength(3);
});

Authentification

Authentification de base

test('basic authentication', async ({ page }) => {
  // Set basic auth
  await page.setExtraHTTPHeaders({
    'Authorization': 'Basic ' + Buffer.from('username:password').toString('base64')
  });

  await page.goto('https://example.com/protected');

  // Or use context-level auth
  const context = await browser.newContext({
    httpCredentials: {
      username: 'testuser',
      password: 'password123'
    }
  });

  const page2 = await context.newPage();
  await page2.goto('https://example.com/protected');
});

Gestion des séances

test('session management', async ({ page }) => {
  // Login
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login');

  // Save session
  const cookies = await page.context().cookies();
  const localStorage = await page.evaluate(() => {
    return JSON.stringify(window.localStorage);
  });

  // Use session in another test
  await page.context().addCookies(cookies);
  await page.evaluate(storage => {
    const data = JSON.parse(storage);
    for (const [key, value] of Object.entries(data)) {
      window.localStorage.setItem(key, value);
    }
  }, localStorage);

  await page.goto('https://example.com/dashboard');
  await expect(page.locator('.user-info')).toBeVisible();
});

Authentification d'authentification et de jetons

test('token authentication', async ({ page }) => {
  // Set authorization header
  await page.setExtraHTTPHeaders({
    'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
  });

  // Or intercept requests to add token
  await page.route('**/api/**', route => {
    route.continue({
      headers: {
        ...route.request().headers(),
        'Authorization': 'Bearer ' + process.env.API_TOKEN
      }
    });
  });

  await page.goto('https://example.com/app');
});

Authentification multi-facteurs

test('multi-factor authentication', async ({ page }) => {
  // Step 1: Username and password
  await page.goto('https://example.com/login');
  await page.fill('#username', 'testuser');
  await page.fill('#password', 'password123');
  await page.click('#login');

  // Step 2: Wait for MFA prompt
  await expect(page.locator('#mfa-code')).toBeVisible();

  // Step 3: Enter MFA code (you might need to generate this)
  const mfaCode = '123456'; // In real tests, this might come from a TOTP generator
  await page.fill('#mfa-code', mfaCode);
  await page.click('#verify-mfa');

  // Step 4: Verify successful login
  await expect(page).toHaveURL(/dashboard/);
  await expect(page.locator('.welcome-message')).toBeVisible();
});

Essais mobiles

Emulation du périphérique

import { test, devices } from '@playwright/test';

test('mobile testing', async ({ browser }) => {
  // iPhone 12
  const iPhone = devices['iPhone 12'];
  const context = await browser.newContext({
    ...iPhone
  });
  const page = await context.newPage();

  await page.goto('https://example.com');

  // Test mobile-specific features
  await expect(page.locator('.mobile-menu')).toBeVisible();
  await expect(page.locator('.desktop-menu')).toBeHidden();

  await context.close();
});

// Using project configuration
test.describe('Mobile Tests', () => {
  test.use({ ...devices['iPhone 12'] });

  test('mobile navigation', async ({ page }) => {
    await page.goto('https://example.com');

    // Test hamburger menu
    await page.click('.hamburger-menu');
    await expect(page.locator('.mobile-nav')).toBeVisible();
  });
});

Interactions tactiles

test('touch interactions', async ({ page }) => {
  // Set mobile viewport
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('https://example.com');

  // Tap (equivalent to click on mobile)
  await page.tap('#button');

  // Long press
  await page.touchscreen.tap(100, 100, { duration: 1000 });

  // Swipe
  await page.touchscreen.tap(100, 300);
  await page.mouse.move(300, 300);
  await page.mouse.up();

  // Pinch to zoom
  await page.touchscreen.tap(200, 200);
  await page.touchscreen.tap(250, 250);

  // Scroll
  await page.mouse.wheel(0, 100);

  // Test responsive design
  const isMobileMenuVisible = await page.isVisible('.mobile-menu');
  expect(isMobileMenuVisible).toBe(true);
});

Essai de géolocalisation

test('geolocation testing', async ({ context, page }) => {
  // Grant geolocation permission
  await context.grantPermissions(['geolocation']);

  // Set geolocation
  await context.setGeolocation({ latitude: 40.7128, longitude: -74.0060 });

  await page.goto('https://example.com/map');

  // Test location-based features
  await page.click('#get-location');
  await expect(page.locator('#location-display')).toContainText('New York');

  // Change location
  await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
  await page.reload();
  await page.click('#get-location');
  await expect(page.locator('#location-display')).toContainText('Los Angeles');
});

Cadre d'essai

Organisation des essais

import { test, expect } from '@playwright/test';

// Test suite
test.describe('User Authentication', () => {
  test.beforeAll(async () => {
    // Setup before all tests in this describe block
    console.log('Setting up test data');
  });

  test.beforeEach(async ({ page }) => {
    // Setup before each test
    await page.goto('https://example.com/login');
  });

  test.afterEach(async ({ page }) => {
    // Cleanup after each test
    await page.evaluate(() => localStorage.clear());
  });

  test.afterAll(async () => {
    // Cleanup after all tests
    console.log('Cleaning up test data');
  });

  test('should login with valid credentials', async ({ page }) => {
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login');

    await expect(page).toHaveURL(/dashboard/);
  });

  test('should show error for invalid credentials', async ({ page }) => {
    await page.fill('#username', 'invalid');
    await page.fill('#password', 'wrong');
    await page.click('#login');

    await expect(page.locator('.error')).toBeVisible();
  });
});

Essais paramétrés

const testData = [
  { username: 'user1', password: 'pass1', expected: 'dashboard' },
  { username: 'user2', password: 'pass2', expected: 'admin' },
  { username: 'user3', password: 'pass3', expected: 'profile' }
];

testData.forEach(({ username, password, expected }) => {
  test(`login with ${username}`, async ({ page }) => {
    await page.goto('https://example.com/login');
    await page.fill('#username', username);
    await page.fill('#password', password);
    await page.click('#login');

    await expect(page).toHaveURL(new RegExp(expected));
  });
});

// Or using test.describe.parallel for parallel execution
test.describe.parallel('Parallel Tests', () => {
  ['chrome', 'firefox', 'safari'].forEach(browser => {
    test(`test on ${browser}`, async ({ page }) => {
      // Test logic here
    });
  });
});

Accessoires personnalisés

// fixtures.js
import { test as base } from '@playwright/test';

// Custom fixture for authenticated user
const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    // Login before each test
    await page.goto('https://example.com/login');
    await page.fill('#username', 'testuser');
    await page.fill('#password', 'password123');
    await page.click('#login');
    await page.waitForURL('**/dashboard');

    await use(page);

    // Logout after each test
    await page.click('#logout');
  }
});

// Use custom fixture
test('dashboard test', async ({ authenticatedPage }) => {
  await expect(authenticatedPage.locator('.welcome')).toBeVisible();
});

export { test };

Annotations d'essai

test('slow test', async ({ page }) => {
  test.slow(); // Mark test as slow (3x timeout)

  await page.goto('https://example.com');
  // Long-running test logic
});

test('flaky test', async ({ page }) => {
  test.fixme(); // Mark test as broken

  await page.goto('https://example.com');
  // Flaky test logic
});

test('skip on mobile', async ({ page, isMobile }) => {
  test.skip(isMobile, 'Not supported on mobile');

  await page.goto('https://example.com');
  // Desktop-only test logic
});

test('conditional test', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Not supported in Safari');

  await page.goto('https://example.com');
  // Browser-specific test logic
});

Déboguement

Mode de débogage

# Run tests in debug mode
npx playwright test --debug

# Debug specific test
npx playwright test tests/example.spec.js --debug

# Debug with headed browser
npx playwright test --headed --debug

# Debug with slow motion
npx playwright test --headed --slowMo=1000

Déboguer dans le code

test('debugging example', async ({ page }) => {
  await page.goto('https://example.com');

  // Pause execution
  await page.pause();

  // Add console logs
  console.log('Current URL:', page.url());

  // Take screenshot for debugging
  await page.screenshot({ path: 'debug-screenshot.png' });

  // Evaluate JavaScript in browser context
  const title = await page.evaluate(() => document.title);
  console.log('Page title:', title);

  // Get element information
  const element = page.locator('#button');
  console.log('Element visible:', await element.isVisible());
  console.log('Element text:', await element.textContent());

  await page.click('#button');
});

Affichage des traces

// playwright.config.js
export default defineConfig({
  use: {
    // Collect trace when retrying the failed test
    trace: 'on-first-retry',

    // Or collect trace for all tests
    trace: 'on',
  },
});

// View traces
// npx playwright show-trace test-results/example-chromium/trace.zip

Outils de développeur de navigateur

test('debug with devtools', async ({ page }) => {
  // Launch browser with devtools
  await page.goto('https://example.com');

  // Open devtools programmatically
  await page.evaluate(() => {
    debugger; // This will trigger debugger if devtools is open
  });

  // Inspect element
  await page.locator('#element').highlight();

  // Log to browser console
  await page.evaluate(() => {
    console.log('Debug message from browser');
  });
});

Gestion des erreurs

test('error handling', async ({ page }) => {
  try {
    await page.goto('https://example.com');
    await page.click('#non-existent-element', { timeout: 5000 });
  } catch (error) {
    console.log('Error occurred:', error.message);

    // Take screenshot on error
    await page.screenshot({ path: 'error-screenshot.png' });

    // Log page content for debugging
    const content = await page.content();
    console.log('Page content:', content);

    throw error; // Re-throw to fail the test
  }
});

Essais de performance

Mesure des performances

test('performance metrics', async ({ page }) => {
  await page.goto('https://example.com');

  // Get performance metrics
  const metrics = await page.evaluate(() => {
    const navigation = performance.getEntriesByType('navigation')[0];
    return {
      domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
      firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,
      firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime
    };
  });

  console.log('Performance metrics:', metrics);

  // Assert performance thresholds
  expect(metrics.domContentLoaded).toBeLessThan(2000);
  expect(metrics.loadComplete).toBeLessThan(5000);
});

Intégration des phares

import { playAudit } from 'playwright-lighthouse';

test('lighthouse audit', async ({ page }) => {
  await page.goto('https://example.com');

  await playAudit({
    page,
    thresholds: {
      performance: 90,
      accessibility: 90,
      'best-practices': 90,
      seo: 90,
    },
    port: 9222,
  });
});

Performance du réseau

test('network performance', async ({ page }) => {
  const responses = [];

  page.on('response', response => {
    responses.push({
      url: response.url(),
      status: response.status(),
      size: response.headers()['content-length'],
      timing: response.timing()
    });
  });

  await page.goto('https://example.com');

  // Analyze network performance
  const slowRequests = responses.filter(r => r.timing.responseEnd > 1000);
  const largeRequests = responses.filter(r => parseInt(r.size) > 1000000);

  expect(slowRequests.length).toBeLessThan(3);
  expect(largeRequests.length).toBeLessThan(2);

  console.log('Total requests:', responses.length);
  console.log('Slow requests:', slowRequests.length);
  console.log('Large requests:', largeRequests.length);
});

Essai de l'API

Essai de l'API REST

import { test, expect, request } from '@playwright/test';

test('API testing', async ({ request }) => {
  // GET request
  const response = await request.get('https://api.example.com/users');
  expect(response.status()).toBe(200);

  const users = await response.json();
  expect(users).toHaveLength(10);

  // POST request
  const newUser = {
    name: 'John Doe',
    email: 'john@example.com'
  };

  const createResponse = await request.post('https://api.example.com/users', {
    data: newUser
  });

  expect(createResponse.status()).toBe(201);
  const createdUser = await createResponse.json();
  expect(createdUser.name).toBe(newUser.name);

  // PUT request
  const updateResponse = await request.put(`https://api.example.com/users/${createdUser.id}`, {
    data: { name: 'Jane Doe' }
  });

  expect(updateResponse.status()).toBe(200);

  // DELETE request
  const deleteResponse = await request.delete(`https://api.example.com/users/${createdUser.id}`);
  expect(deleteResponse.status()).toBe(204);
});

authentification API

test('API with authentication', async ({ request }) => {
  // Login to get token
  const loginResponse = await request.post('https://api.example.com/auth/login', {
    data: {
      username: 'testuser',
      password: 'password123'
    }
  });

  const { token } = await loginResponse.json();

  // Use token for authenticated requests
  const response = await request.get('https://api.example.com/protected', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  expect(response.status()).toBe(200);
});

Essai de GraphQL

test('GraphQL API testing', async ({ request }) => {
  const query = `
    query GetUsers {
      users {
        id
        name
        email
      }
    }
  `;

  const response = await request.post('https://api.example.com/graphql', {
    data: { query }
  });

  expect(response.status()).toBe(200);

  const result = await response.json();
  expect(result.data.users).toBeDefined();
  expect(result.data.users.length).toBeGreaterThan(0);
});

Essais visuels

Essai de régression visuelle

test('visual regression', async ({ page }) => {
  await page.goto('https://example.com');

  // Take baseline screenshot
  await expect(page).toHaveScreenshot('homepage.png');

  // Test different states
  await page.hover('#menu');
  await expect(page).toHaveScreenshot('homepage-menu-hover.png');

  await page.click('#toggle-theme');
  await expect(page).toHaveScreenshot('homepage-dark-theme.png');

  // Test responsive design
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(page).toHaveScreenshot('homepage-tablet.png');

  await page.setViewportSize({ width: 375, height: 667 });
  await expect(page).toHaveScreenshot('homepage-mobile.png');
});

Essai visuel des composants

test('component visual testing', async ({ page }) => {
  await page.goto('https://example.com/components');

  // Test individual components
  await expect(page.locator('.button')).toHaveScreenshot('button.png');
  await expect(page.locator('.card')).toHaveScreenshot('card.png');
  await expect(page.locator('.modal')).toHaveScreenshot('modal.png');

  // Test component states
  await page.hover('.button');
  await expect(page.locator('.button')).toHaveScreenshot('button-hover.png');

  await page.click('.button');
  await expect(page.locator('.button')).toHaveScreenshot('button-active.png');
});

Essais visuels des navigateurs croisés

['chromium', 'firefox', 'webkit'].forEach(browserName => {
  test(`visual test on ${browserName}`, async ({ page }) => {
    await page.goto('https://example.com');

    await expect(page).toHaveScreenshot(`homepage-${browserName}.png`);
  });
});

Intégration CI/CD

Actions GitHub

# .github/workflows/playwright.yml
name: Playwright Tests
on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
      with:
        node-version: 18
    - name: Install dependencies
      run: npm ci
    - name: Install Playwright Browsers
      run: npx playwright install --with-deps
    - name: Run Playwright tests
      run: npx playwright test
    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

Intégration Docker

# Dockerfile
FROM mcr.microsoft.com/playwright:v1.40.0-focal

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]
# Build and run tests in Docker
docker build -t playwright-tests .
docker run --rm playwright-tests

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Install Dependencies') {
            steps {
                sh 'npm ci'
                sh 'npx playwright install --with-deps'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'npx playwright test'
            }
            post {
                always {
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: false,
                        keepAll: true,
                        reportDir: 'playwright-report',
                        reportFiles: 'index.html',
                        reportName: 'Playwright Report'
                    ])
                }
            }
        }
    }
}

Caractéristiques avancées

Correspondants personnalisés

// custom-matchers.js
import { expect } from '@playwright/test';

expect.extend({
  async toHaveLoadTime(page, maxTime) {
    const startTime = Date.now();
    await page.waitForLoadState('networkidle');
    const loadTime = Date.now() - startTime;

    const pass = loadTime <= maxTime;

    return {
      message: () => `Expected load time to be <= ${maxTime}ms, but was ${loadTime}ms`,
      pass
    };
  }
});

// Usage
test('custom matcher', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveLoadTime(3000);
});

Modèle d'objet de page

// pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('#username');
    this.passwordInput = page.locator('#password');
    this.loginButton = page.locator('#login');
    this.errorMessage = page.locator('.error');
  }

  async goto() {
    await this.page.goto('https://example.com/login');
  }

  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }
}

// Usage in test
import { LoginPage } from './pages/LoginPage';

test('login with page object', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.goto();
  await loginPage.login('testuser', 'password123');

  await expect(page).toHaveURL(/dashboard/);
});

Mise en place et mise à l'eau à l'échelle mondiale

// global-setup.js
export default async function globalSetup() {
  // Start test server
  console.log('Starting test server...');

  // Create test data
  console.log('Creating test data...');

  // Any other global setup
}

// global-teardown.js
export default async function globalTeardown() {
  // Stop test server
  console.log('Stopping test server...');

  // Clean up test data
  console.log('Cleaning up test data...');
}

// playwright.config.js
export default defineConfig({
  globalSetup: require.resolve('./global-setup'),
  globalTeardown: require.resolve('./global-teardown'),
});

Meilleures pratiques

Organisation des essais

// Organize tests by feature/page
test.describe('User Management', () => {
  test.describe('User Registration', () => {
    test('should register new user', async ({ page }) => {
      // Test implementation
    });

    test('should validate email format', async ({ page }) => {
      // Test implementation
    });
  });

  test.describe('User Login', () => {
    test('should login with valid credentials', async ({ page }) => {
      // Test implementation
    });
  });
});

Sélecteurs fiables

// Good: Use data-testid attributes
await page.click('[data-testid="submit-button"]');

// Good: Use role-based selectors
await page.click('role=button[name="Submit"]');

// Good: Use text content
await page.click('text=Submit');

// Avoid: CSS selectors that depend on styling
await page.click('.btn.btn-primary.submit-btn');

// Avoid: XPath with position
await page.click('//div[3]/button[2]');

Gestion des erreurs

test('robust error handling', async ({ page }) => {
  try {
    await page.goto('https://example.com');

    // Use soft assertions for non-critical checks
    await expect.soft(page.locator('.optional-element')).toBeVisible();

    // Continue with critical test steps
    await page.click('#important-button');
    await expect(page.locator('#result')).toBeVisible();

  } catch (error) {
    // Take screenshot on failure
    await page.screenshot({ path: 'failure-screenshot.png' });

    // Log additional context
    console.log('Current URL:', page.url());
    console.log('Page title:', await page.title());

    throw error;
  }
});

Meilleures pratiques en matière de rendement

  • Utilisez l'attente automatique: Playwright attend automatiquement les éléments
  • Éviter les attentes inutiles: N'utilisez pas page.waitForTimeout() sauf si cela est absolument nécessaire
  • Réutiliser les contextes du navigateur: Partager les contextes entre les essais lorsque c'est possible
  • Tests de parallélisation: Utiliser fullyParallel: true dans la configuration
  • Optimiser les sélecteurs: Utiliser des sélecteurs efficaces comme data-testid

Pratiques exemplaires de maintenance

  • ** Garder les essais indépendants**: Chaque essai doit pouvoir être exécuté isolément
  • Utiliser le modèle d'objet de page : Encapsuler les interactions de pages dans les classes réutilisables
  • Mise en œuvre d'un nettoyage approprié: État clair entre les essais
  • Utilisez des noms de test significatifs: Décrivez ce que le test vérifie
  • ** Mises à jour périodiques** : Gardez Playwright et les navigateurs mis à jour

Résumé

Playwright est un cadre de test web puissant et moderne qui fournit:

  • **Support de navigation sur le chrome, Firefox et WebKit
  • En attente automatique: Intelligent attendant que les éléments soient prêts
  • Sélecteurs puissants: plusieurs stratégies de sélection, y compris basées sur le rôle
  • Interception du réseau: Mock et surveiller les demandes de réseau
  • Essais mobiles: Emulation des appareils et interactions tactiles
  • ** Essais visuels**: comparaison des captures d'écran et essais de régression visuelle
  • Outils de débogage: Intégration d'outils de visionnage, de débogage et de développement
  • CI/CD Ready: Intégration facile avec les plateformes de CI/CD populaires
  • API Testing: Support intégré pour les tests d'API REST et GraphQL
  • ** Test de performance** : mesures de performance intégrées et intégration de phares

L'architecture moderne de Playwright et son ensemble complet de fonctionnalités en font un excellent choix pour les tests de bout en bout, l'automatisation du navigateur et les tâches de grattage web.