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.
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();
});
Navigateurs multiples¶
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();
}
});
Navigation des pages¶
Navigation de base¶
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"]
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: truedans 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.