Framework de Test E2E Playwright
Exemples complets de tests Playwright incluant automatisation multi-navigateur, tests d'API, tests mobiles, tests de régression visuelle et patterns E2E avancés pour applications web modernes
Key Facts
- Category
- Testing
- Items
- 4
- Format Families
- text
Sample Overview
Exemples complets de tests Playwright incluant automatisation multi-navigateur, tests d'API, tests mobiles, tests de régression visuelle et patterns E2E avancés pour applications web modernes This sample set belongs to Testing and can be used to test related workflows inside Elysia Tools.
💻 Configuration de Projet Playwright text
Configuration complète de projet Playwright avec configuration TypeScript, structure de tests, fixtures et meilleures pratiques de tests E2E fiables
// Playwright Project Setup and Configuration
// 1. playwright.config.ts - Main configuration
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
const playwrightMainConfig = 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'],
['json', { outputFile: 'test-results.json' }],
['junit', { outputFile: 'test-results.xml' }],
['allure-playwright']
],
// Global setup and teardown
globalSetup: require.resolve('./tests/global-setup.ts'),
globalTeardown: require.resolve('./tests/global-teardown.ts'),
// Use shared folder for test assets
use: {
// Base URL to use in actions like `await page.goto('/')`
baseURL: process.env.BASE_URL || 'http://localhost:3000',
// Collect trace when retrying the failed test
trace: 'on-first-retry',
// Record video only when retrying a test for the first time
video: 'retain-on-failure',
// Take screenshot on failure
screenshot: 'only-on-failure',
// Global timeout for each action
actionTimeout: 10000,
// Global timeout for navigation
navigationTimeout: 30000,
// Viewport size
viewport: { width: 1280, height: 720 },
// Ignore HTTPS errors
ignoreHTTPSErrors: true,
// User agent
userAgent: 'Playwright Test Bot'
},
// Configure projects for major browsers
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Test against mobile viewports
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
// Test against branded browsers
{
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
},
],
// Run your local dev server before starting the tests
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
// Global test timeout
timeout: 30000,
// Expect timeout
expect: {
timeout: 5000
}
});
// 2. package.json dependencies
{
"devDependencies": {
"@playwright/test": "^1.40.0",
"@types/node": "^20.8.0",
"typescript": "^5.2.0",
"allure-playwright": "^2.1.0",
"@playwright/test-expect": "^1.40.0",
"playwright-lighthouse": "^2.2.0",
"axe-playwright": "^2.0.0"
},
"scripts": {
"test": "playwright test",
"test:ui": "playwright test --ui",
"test:debug": "playwright test --debug",
"test:codegen": "playwright codegen",
"test:report": "playwright show-report",
"test:install": "playwright install",
"test:headed": "playwright test --headed",
"test:project": "playwright test --project=chromium",
"test:mobile": "playwright test --project="Mobile Chrome"",
"test:api": "playwright test --config=playwright.api.config.ts"
}
}
// 3. tests/fixtures/base.fixture.ts - Base test fixture
import { test as base, expect } from '@playwright/test';
import { HomePage } from '../pages/HomePage';
import { LoginPage } from '../pages/LoginPage';
import { ApiHelper } from '../utils/ApiHelper';
// Define custom fixture types
type TestFixtures = {
homePage: HomePage;
loginPage: LoginPage;
apiHelper: ApiHelper;
authenticatedPage: {
page: Page;
token: string;
};
};
// Extend base test with custom fixtures
export const test = base.extend<TestFixtures>({
// Custom home page fixture
homePage: async ({ page }, use) => {
const homePage = new HomePage(page);
await use(homePage);
},
// Custom login page fixture
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
// API helper fixture
apiHelper: async ({ request }, use) => {
const apiHelper = new ApiHelper(request);
await use(apiHelper);
},
// Authenticated page fixture
authenticatedPage: async ({ page, apiHelper }, use) => {
// Login via API and set auth token
const loginResponse = await apiHelper.login('testuser', 'testpass');
const token = loginResponse.token;
// Set token in localStorage
await page.goto('/');
await page.evaluate(([authToken]) => {
localStorage.setItem('authToken', authToken);
}, [token]);
await use({ page, token });
// Cleanup: remove auth token
await page.evaluate(() => {
localStorage.removeItem('authToken');
});
}
});
// Export expect for use in tests
export { expect };
// Export test types
export type { TestFixtures };
// 4. tests/global-setup.ts - Global test setup
import { chromium, FullConfig } from '@playwright/test';
import { ApiHelper } from './utils/ApiHelper';
async function globalSetup(config: FullConfig) {
console.log('🚀 Starting global setup...');
const browser = await chromium.launch();
const context = await browser.newContext();
const apiHelper = new ApiHelper(context.request);
try {
// Setup test data
console.log('📊 Setting up test data...');
await apiHelper.setupTestData();
// Create test users
console.log('👤 Creating test users...');
await apiHelper.createUser({
username: 'testuser',
email: '[email protected]',
password: 'testpass',
role: 'user'
});
await apiHelper.createUser({
username: 'admin',
email: '[email protected]',
password: 'adminpass',
role: 'admin'
});
// Setup test database state
console.log('🗄️ Setting up database...');
await apiHelper.resetDatabase();
console.log('✅ Global setup completed successfully');
} catch (error) {
console.error('❌ Global setup failed:', error);
throw error;
} finally {
await context.close();
await browser.close();
}
}
export { globalSetup };
// 5. tests/global-teardown.ts - Global test cleanup
import { chromium, FullConfig } from '@playwright/test';
import { ApiHelper } from './utils/ApiHelper';
async function globalTeardown(config: FullConfig) {
console.log('🧹 Starting global teardown...');
const browser = await chromium.launch();
const context = await browser.newContext();
const apiHelper = new ApiHelper(context.request);
try {
// Cleanup test data
console.log('🗑️ Cleaning up test data...');
await apiHelper.cleanupTestData();
// Remove test users
console.log('👥 Removing test users...');
await apiHelper.deleteUser('testuser');
await apiHelper.deleteUser('admin');
// Close database connections
console.log('🔌 Closing database connections...');
await apiHelper.closeDatabase();
console.log('✅ Global teardown completed successfully');
} catch (error) {
console.error('❌ Global teardown failed:', error);
throw error;
} finally {
await context.close();
await browser.close();
}
}
export { globalTeardown };
// 6. Basic test structure
// tests/basic/homepage.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe('Homepage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display correct title', async ({ page }) => {
await expect(page).toHaveTitle(/Playwright Testing Demo/);
});
test('should display main navigation', async ({ page }) => {
const navigation = page.locator('[data-testid=main-navigation]');
await expect(navigation).toBeVisible();
// Check navigation links
await expect(page.getByRole('link', { name: 'Home' })).toBeVisible();
await expect(page.getByRole('link', { name: 'About' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Contact' })).toBeVisible();
});
test('should display hero section', async ({ page }) => {
const heroSection = page.locator('[data-testid=hero-section]');
await expect(heroSection).toBeVisible();
await expect(heroSection.getByRole('heading', { level: 1 }))
.toContainText('Welcome to Playwright Testing');
await expect(page.getByRole('button', { name: 'Get Started' }))
.toBeVisible();
});
test('should be responsive on different viewports', async ({ page }) => {
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.locator('[data-testid=mobile-menu]')).toBeVisible();
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await expect(page.locator('[data-testid=tablet-layout]')).toBeVisible();
// Test desktop viewport
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.locator('[data-testid=desktop-layout]')).toBeVisible();
});
});
// 7. Page Object Model example
// tests/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
// Common locators
readonly header = this.page.locator('[data-testid=header]');
readonly footer = this.page.locator('[data-testid=footer]');
readonly navigation = this.page.locator('[data-testid=main-navigation]');
readonly loadingSpinner = this.page.locator('[data-testid=loading-spinner]');
// Common methods
async navigateTo(url: string) {
await this.page.goto(url);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
async waitForElementToDisappear(locator: Locator, timeout = 5000) {
await expect(locator).not.toBeVisible({ timeout });
}
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
async scrollToElement(locator: Locator) {
await locator.scrollIntoViewIfNeeded();
}
async waitForApiCall(apiPattern: string, timeout = 30000) {
return await this.page.waitForResponse(response =>
response.url().includes(apiPattern),
{ timeout }
);
}
// Helper method to wait for animations
async waitForAnimations() {
await this.page.waitForFunction(() => {
return document.getAnimations().length === 0;
});
}
// Method to check accessibility
async checkAccessibility() {
// This would integrate with axe-core or similar
console.log('Checking accessibility...');
}
}
// tests/pages/HomePage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
// Locators
readonly heroTitle = this.page.locator('[data-testid=hero-title]');
readonly heroSubtitle = this.page.locator('[data-testid=hero-subtitle]');
readonly getStartedButton = this.page.getByRole('button', { name: 'Get Started' });
readonly featuresSection = this.page.locator('[data-testid=features-section]');
readonly testimonialSection = this.page.locator('[data-testid=testimonial-section]');
// Feature cards
readonly featureCards = this.page.locator('[data-testid=feature-card]');
readonly firstFeatureCard = this.featureCards.first();
constructor(page: Page) {
super(page);
}
async visit() {
await this.navigateTo('/');
await this.waitForPageLoad();
}
async getStarted() {
await this.getStartedButton.click();
}
async verifyHeroSection() {
await expect(this.heroTitle).toBeVisible();
await expect(this.heroTitle).toContainText('Welcome');
await expect(this.heroSubtitle).toBeVisible();
await expect(this.getStartedButton).toBeVisible();
}
async verifyFeaturesSection() {
await expect(this.featuresSection).toBeVisible();
await expect(this.featureCards).toHaveCount(3);
}
async verifyTestimonialSection() {
await expect(this.testimonialSection).toBeVisible();
}
async navigateToFeature(featureName: string) {
const featureCard = this.featureCards.filter({ hasText: featureName }).first();
await featureCard.click();
}
}
// tests/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
// Locators
readonly usernameInput = this.page.locator('[data-testid=username-input]');
readonly passwordInput = this.page.locator('[data-testid=password-input]');
readonly loginButton = this.page.getByRole('button', { name: 'Login' });
readonly errorMessage = this.page.locator('[data-testid=error-message]');
readonly forgotPasswordLink = this.page.getByRole('link', { name: 'Forgot Password?' });
readonly rememberMeCheckbox = this.page.locator('[data-testid=remember-me]');
constructor(page: Page) {
super(page);
}
async visit() {
await this.navigateTo('/login');
await this.waitForPageLoad();
}
async login(username: string, password: string, rememberMe = false) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
if (rememberMe) {
await this.rememberMeCheckbox.check();
}
await this.loginButton.click();
}
async verifyLoginPage() {
await expect(this.usernameInput).toBeVisible();
await expect(this.passwordInput).toBeVisible();
await expect(this.loginButton).toBeVisible();
await expect(this.forgotPasswordLink).toBeVisible();
await expect(this.rememberMeCheckbox).toBeVisible();
}
async getErrorMessage(): Promise<string> {
await expect(this.errorMessage).toBeVisible();
return await this.errorMessage.textContent() || '';
}
async verifySuccessfulLogin() {
// Check that we've redirected away from login page
await expect(this.page).not.toHaveURL('/login');
// Check for user menu or other logged-in indicators
const userMenu = this.page.locator('[data-testid=user-menu]');
await expect(userMenu).toBeVisible();
}
async forgotPassword() {
await this.forgotPasswordLink.click();
}
}
// 8. Using Page Objects in tests
// tests/authentication/login.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe('Authentication', () => {
test('should login successfully with valid credentials', async ({ loginPage }) => {
await loginPage.visit();
await loginPage.verifyLoginPage();
await loginPage.login('testuser', 'testpass');
await loginPage.verifySuccessfulLogin();
});
test('should show error with invalid credentials', async ({ loginPage }) => {
await loginPage.visit();
await loginPage.verifyLoginPage();
await loginPage.login('invaliduser', 'invalidpass');
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).toContain('Invalid credentials');
});
test('should remember user when checkbox is checked', async ({ loginPage, page }) => {
await loginPage.visit();
await loginPage.login('testuser', 'testpass', true);
await loginPage.verifySuccessfulLogin();
// Check that remember me cookie is set
const cookies = await page.context().cookies();
const rememberMeCookie = cookies.find(cookie => cookie.name === 'remember_me');
expect(rememberMeCookie).toBeDefined();
});
});
// 9. Test data management
// tests/fixtures/testData.json
{
"users": {
"validUser": {
"username": "testuser",
"password": "testpass",
"email": "[email protected]"
},
"invalidUser": {
"username": "invaliduser",
"password": "wrongpass"
},
"adminUser": {
"username": "admin",
"password": "adminpass",
"role": "administrator"
}
},
"products": [
{
"id": 1,
"name": "Test Product 1",
"price": 99.99,
"category": "Electronics"
},
{
"id": 2,
"name": "Test Product 2",
"price": 49.99,
"category": "Books"
}
],
"testUrls": {
"homepage": "/",
"login": "/login",
"dashboard": "/dashboard",
"products": "/products"
}
}
// tests/utils/TestDataLoader.ts
import testData from '../fixtures/testData.json';
export class TestDataLoader {
static getUser(userType: keyof typeof testData.users) {
return testData.users[userType];
}
static getProduct(productId: number) {
return testData.products.find(p => p.id === productId);
}
static getAllProducts() {
return testData.products;
}
static getUrl(urlName: keyof typeof testData.testUrls) {
return testData.testUrls[urlName];
}
}
// 10. Using test data in tests
// tests/product-management/products.spec.ts
import { test, expect } from '../fixtures/base.fixture';
import { TestDataLoader } from '../utils/TestDataLoader';
test.describe('Product Management', () => {
test('should display product list', async ({ page }) => {
await page.goto(TestDataLoader.getUrl('products'));
const products = TestDataLoader.getAllProducts();
for (const product of products) {
await expect(page.locator(`[data-testid=product-${product.id}]`))
.toContainText(product.name);
await expect(page.locator(`[data-testid=price-${product.id}]`))
.toContainText(`$${product.price}`);
}
});
test('should search for products', async ({ page }) => {
await page.goto(TestDataLoader.getUrl('products'));
const searchInput = page.locator('[data-testid=search-input]');
const searchButton = page.getByRole('button', { name: 'Search' });
const product = TestDataLoader.getProduct(1);
await searchInput.fill(product.name);
await searchButton.click();
await expect(page.locator('[data-testid=product-1]'))
.toContainText(product.name);
await expect(page.locator('[data-testid=product-2]'))
.not.toBeVisible();
});
});
💻 Tests Multi-Navigateur et Multi-Plateforme text
Exemples complets de tests multi-navigateur incluant tests mobiles, vérification de design responsive et stratégies de tests spécifiques à la plateforme
// Cross-Browser and Cross-Platform Testing with Playwright
// 1. Cross-browser test configuration
// playwright.config.ts - Cross-browser projects
import { defineConfig, devices } from '@playwright/test';
const playwrightApiConfig = defineConfig({
projects: [
// Desktop browsers
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
testIgnore: '**/mobile-only.spec.ts',
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
testIgnore: '**/mobile-only.spec.ts',
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
testIgnore: '**/mobile-only.spec.ts',
},
{
name: 'edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
testIgnore: '**/mobile-only.spec.ts',
},
// Mobile browsers
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
testMatch: '**/mobile-only.spec.ts',
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 12'] },
testMatch: '**/mobile-only.spec.ts',
},
// Tablet browsers
{
name: 'tablet-chrome',
use: { ...devices['iPad Pro'] },
},
// Custom viewport testing
{
name: 'custom-viewport',
use: {
...devices['Desktop Chrome'],
viewport: { width: 800, height: 600 }
},
}
]
});
// 2. Cross-browser compatibility tests
// tests/cross-browser/browser-compatibility.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe.configure({ mode: 'parallel' });
test.describe('Cross-Browser Compatibility', () => {
test('should render consistently across all browsers', async ({ page }) => {
await page.goto('/');
// Basic layout checks
await expect(page.locator('h1')).toBeVisible();
await expect(page.locator('nav')).toBeVisible();
await expect(page.locator('main')).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
});
test('should handle CSS Grid consistently', async ({ page }) => {
await page.goto('/grid-layout');
// Test grid container
const gridContainer = page.locator('[data-testid=grid-container]');
await expect(gridContainer).toBeVisible();
// Test grid items
const gridItems = gridContainer.locator('[data-testid=grid-item]');
await expect(gridItems).toHaveCount(6);
// Verify grid layout with bounding box checks
const firstItem = gridItems.first();
const firstItemBox = await firstItem.boundingBox();
expect(firstItemBox).toBeTruthy();
const lastItem = gridItems.last();
const lastItemBox = await lastItem.boundingBox();
expect(lastItemBox).toBeTruthy();
// Verify items don't overlap
expect(lastItemBox!.y).toBeGreaterThan(firstItemBox!.y);
});
test('should handle Flexbox consistently', async ({ page }) => {
await page.goto('/flexbox-layout');
const flexContainer = page.locator('[data-testid=flex-container]');
await expect(flexContainer).toBeVisible();
// Test flex items alignment
const flexItems = flexContainer.locator('[data-testid=flex-item]');
await expect(flexItems).toHaveCount(4);
// Check vertical alignment
const firstItemBox = await flexItems.first().boundingBox();
const lastItemBox = await flexItems.last().boundingBox();
expect(Math.abs(firstItemBox!.y - lastItemBox!.y)).toBeLessThan(5);
});
test('should handle forms consistently', async ({ page }) => {
await page.goto('/forms');
// Test form elements
await expect(page.locator('input[type="text"]')).toBeVisible();
await expect(page.locator('input[type="email"]')).toBeVisible();
await expect(page.locator('input[type="password"]')).toBeVisible();
await expect(page.locator('select')).toBeVisible();
await expect(page.locator('textarea')).toBeVisible();
await expect(page.locator('input[type="checkbox"]')).toBeVisible();
await expect(page.locator('input[type="radio"]')).toBeVisible();
// Test form submission
await page.fill('input[name="name"]', 'Test User');
await page.fill('input[name="email"]', '[email protected]');
await page.selectOption('select[name="country"]', 'US');
// Checkbox interaction
await page.check('input[type="checkbox"]');
await expect(page.locator('input[type="checkbox"]')).toBeChecked();
// Radio button interaction
await page.check('input[type="radio"][value="option1"]');
await expect(page.locator('input[type="radio"][value="option1"]')).toBeChecked();
});
test('should handle JavaScript features consistently', async ({ page }) => {
await page.goto('/javascript-features');
// Test event listeners
const button = page.locator('[data-testid=click-button"]');
await button.click();
await expect(page.locator('[data-testid=click-count]')).toContainText('1');
// Test async operations
const asyncButton = page.locator('[data-testid=async-button]');
await asyncButton.click();
await expect(page.locator('[data-testid=async-result]')).toBeVisible({ timeout: 5000 });
// Test dynamic content
const dynamicButton = page.locator('[data-testid=dynamic-button]');
await dynamicButton.click();
await expect(page.locator('[data-testid=dynamic-content]')).toContainText('Dynamic Content');
});
});
// 3. Mobile-specific tests
// tests/cross-browser/mobile-only.spec.ts
import { test, expect, devices } from '@playwright/test';
// These tests only run on mobile projects
test.describe('Mobile-Specific Features', () => {
test('should handle touch gestures', async ({ page }) => {
await page.goto('/touch-gestures');
const touchArea = page.locator('[data-testid=touch-area]');
// Test tap
await touchArea.tap();
await expect(page.locator('[data-testid=gesture-result]')).toContainText('tapped');
// Test swipe (using touch events)
await touchArea.tap();
await page.touch.move(touchArea, { position: { x: 100, y: 100 } });
await page.touch.move(touchArea, { position: { x: 200, y: 100 } });
await page.touch.end();
await expect(page.locator('[data-testid=gesture-result]')).toContainText('swiped');
});
test('should handle device orientation', async ({ page }) => {
await page.goto('/orientation-test');
// Test portrait mode
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.locator('[data-testid=orientation-indicator]')).toContainText('portrait');
// Test landscape mode
await page.setViewportSize({ width: 667, height: 375 });
await expect(page.locator('[data-testid=orientation-indicator]')).toContainText('landscape');
});
test('should handle mobile navigation', async ({ page }) => {
await page.goto('/');
// Check for mobile menu button
const menuButton = page.locator('[data-testid=mobile-menu-button]');
await expect(menuButton).toBeVisible();
// Open mobile menu
await menuButton.tap();
const mobileMenu = page.locator('[data-testid=mobile-menu]');
await expect(mobileMenu).toBeVisible();
// Test menu items
await expect(page.locator('[data-testid=menu-item]').first()).toBeVisible();
// Close menu
const closeButton = page.locator('[data-testid=mobile-menu-close]');
await closeButton.tap();
await expect(mobileMenu).not.toBeVisible();
});
test('should handle pull-to-refresh', async ({ page }) => {
await page.goto('/pull-to-refresh');
// Simulate pull to refresh gesture
await page.touch.start(200, 50);
await page.touch.move(200, 250);
await page.touch.end();
await expect(page.locator('[data-testid=refresh-indicator]')).toBeVisible();
await expect(page.locator('[data-testid=refresh-result]')).toContainText('refreshed');
});
test('should handle virtual keyboard', async ({ page }) => {
await page.goto('/form-test');
const inputField = page.locator('input[type="text"]');
await inputField.tap();
// Check if keyboard appears (test for viewport shift)
const initialViewport = page.viewportSize();
await page.waitForTimeout(500); // Wait for keyboard to appear
// In a real test, you might check for viewport changes or keyboard-specific classes
await expect(inputField).toBeFocused();
// Test keyboard dismissal
await page.keyboard.press('Escape');
await expect(inputField).not.toBeFocused();
});
});
// 4. Responsive design tests
// tests/cross-browser/responsive-design.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe('Responsive Design Testing', () => {
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'mobile-large', width: 414, height: 896 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'tablet-large', width: 1024, height: 1366 },
{ name: 'desktop-small', width: 1280, height: 720 },
{ name: 'desktop-large', width: 1920, height: 1080 }
];
viewports.forEach(({ name, width, height }) => {
test.describe(`${name} viewport (${width}x${height})`, () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize({ width, height });
});
test('should adapt navigation layout', async ({ page }) => {
await page.goto('/');
if (width < 768) {
// Mobile layout
await expect(page.locator('[data-testid=mobile-menu-button]')).toBeVisible();
await expect(page.locator('[data-testid=desktop-nav]')).not.toBeVisible();
} else {
// Desktop/tablet layout
await expect(page.locator('[data-testid=desktop-nav]')).toBeVisible();
await expect(page.locator('[data-testid=mobile-menu-button]')).not.toBeVisible();
}
});
test('should adapt content layout', async ({ page }) => {
await page.goto('/content-grid');
const gridContainer = page.locator('[data-testid=content-grid]');
await expect(gridContainer).toBeVisible();
// Check number of columns based on viewport
const gridItems = gridContainer.locator('[data-testid=content-item]');
const itemCount = await gridItems.count();
if (width < 768) {
// Mobile: single column
// Check that items are stacked vertically
const firstItem = gridItems.first();
const secondItem = gridItems.nth(1);
const firstBox = await firstItem.boundingBox();
const secondBox = await secondItem.boundingBox();
expect(secondBox!.y).toBeGreaterThan(firstBox!.y);
expect(Math.abs(secondBox!.x - firstBox!.x)).toBeLessThan(50);
} else if (width < 1024) {
// Tablet: 2 columns
// Check grid layout
const containerBox = await gridContainer.boundingBox();
expect(containerBox!.width).toBeGreaterThan(width * 0.9);
} else {
// Desktop: 3+ columns
const containerBox = await gridContainer.boundingBox();
expect(containerBox!.width).toBeGreaterThan(width * 0.8);
}
});
test('should adapt typography', async ({ page }) => {
await page.goto('/typography');
const heading = page.locator('[data-testid=main-heading]');
await expect(heading).toBeVisible();
// Check font sizes (using computed styles)
const fontSize = await heading.evaluate(el =>
window.getComputedStyle(el).fontSize
);
if (width < 768) {
expect(parseInt(fontSize)).toBeLessThan(32); // Smaller font on mobile
} else {
expect(parseInt(fontSize)).toBeGreaterThanOrEqual(24); // Larger font on desktop
}
});
test('should handle images responsively', async ({ page }) => {
await page.goto('/image-gallery');
const images = page.locator('[data-testid=gallery-image]');
await expect(images.first()).toBeVisible();
// Check image dimensions
const firstImage = images.first();
const imageBox = await firstImage.boundingBox();
expect(imageBox!.width).toBeLessThanOrEqual(width * 0.9); // Should not overflow viewport
// Test image loading
await expect(firstImage).toHaveAttribute('src');
await expect(firstImage).toBeVisible();
});
});
});
});
// 5. Browser-specific feature tests
// tests/cross-browser/browser-features.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Browser-Specific Features', () => {
test.describe('Chrome-specific features', () => {
test.use({ browserName: 'chromium' });
test('should handle Chrome DevTools Protocol', async ({ page }) => {
await page.goto('/');
// Test Chrome-specific features
const client = await page.context().newCDPSession(page);
// Enable performance domain
await client.send('Performance.enable');
// Get performance metrics
const metrics = await client.send('Performance.getMetrics');
expect(metrics.metrics).toBeDefined();
// Test memory usage
const memoryMetrics = metrics.metrics.filter(m => m.name.includes('Memory'));
expect(memoryMetrics.length).toBeGreaterThan(0);
});
test('should handle Chrome extensions', async ({ page, context }) => {
// Load test extension (if available)
try {
await context.addInitScript(() => {
// Simulate extension injection
window.chromeExtensionLoaded = true;
});
await page.goto('/');
await expect(page.evaluate(() => window.chromeExtensionLoaded)).toBeTruthy();
} catch (error) {
console.log('Extension test skipped:', error);
}
});
});
test.describe('Firefox-specific features', () => {
test.use({ browserName: 'firefox' });
test('should handle Firefox preferences', async ({ page }) => {
await page.goto('/');
// Test Firefox-specific CSS
await page.addStyleTag({
content: '@-moz-document url-prefix() { .firefox-only { color: red; } }'
});
const firefoxElement = page.locator('.firefox-only');
if (await firefoxElement.count() > 0) {
await expect(firefoxElement).toHaveCSS('color', 'rgb(255, 0, 0)');
}
});
});
test.describe('Safari/WebKit-specific features', () => {
test.use({ browserName: 'webkit' });
test('should handle WebKit-specific CSS', async ({ page }) => {
await page.goto('/webkit-features');
// Test WebKit-specific features
await page.addStyleTag({
content: '.safari-only { -webkit-appearance: none; -webkit-user-select: none; }'
});
const safariElement = page.locator('.safari-only');
if (await safariElement.count() > 0) {
await expect(safariElement).toBeVisible();
}
});
test('should handle iOS Safari features', async ({ page }) => {
// Test iOS-specific touch handling
await page.goto('/touch-test');
const touchElement = page.locator('[data-testid=touch-element]');
await touchElement.tap();
await expect(page.locator('[data-testid=touch-result]')).toContainText('touched');
});
});
});
// 6. Performance testing across browsers
// tests/cross-browser/performance.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe('Cross-Browser Performance Testing', () => {
test('should load within reasonable time across browsers', async ({ page }) => {
const startTime = Date.now();
await page.goto('/', { waitUntil: 'networkidle' });
const loadTime = Date.now() - startTime;
// Should load within 5 seconds on any browser
expect(loadTime).toBeLessThan(5000);
console.log(`Page load time: ${loadTime}ms`);
});
test('should handle resource loading efficiently', async ({ page }) => {
const responses: any[] = [];
page.on('response', response => {
responses.push({
url: response.url(),
status: response.status(),
timing: response.request().timing()
});
});
await page.goto('/');
// Check for failed requests
const failedRequests = responses.filter(r => r.status >= 400);
expect(failedRequests.length).toBe(0);
// Check for slow resources (> 2 seconds)
const slowResources = responses.filter(r =>
r.timing.responseEnd - r.timing.requestStart > 2000
);
console.log(`Slow resources: ${slowResources.length}`);
// Allow some slow resources but not too many
expect(slowResources.length).toBeLessThan(3);
});
test('should handle JavaScript execution performance', async ({ page }) => {
await page.goto('/performance-test');
// Test complex JavaScript operations
const executionTime = await page.evaluate(() => {
const start = performance.now();
// Simulate heavy computation
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.sqrt(i);
}
const end = performance.now();
return end - start;
});
// Should complete within reasonable time
expect(executionTime).toBeLessThan(1000);
console.log(`JS execution time: ${executionTime.toFixed(2)}ms`);
});
});
// 7. Accessibility testing across browsers
// tests/cross-browser/accessibility.spec.ts
import { test, expect } from '../fixtures/base.fixture';
test.describe('Cross-Browser Accessibility Testing', () => {
test('should maintain accessibility across all browsers', async ({ page }) => {
await page.goto('/accessibility-test');
// Test keyboard navigation
await page.keyboard.press('Tab');
let focusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(['BUTTON', 'INPUT', 'A', 'SELECT']).toContain(focusedElement);
// Continue tabbing through focusable elements
for (let i = 0; i < 5; i++) {
await page.keyboard.press('Tab');
focusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(focusedElement).toBeTruthy();
}
});
test('should handle screen readers consistently', async ({ page }) => {
await page.goto('/accessibility-test');
// Check ARIA labels
const buttons = page.locator('button[aria-label]');
const buttonCount = await buttons.count();
if (buttonCount > 0) {
for (let i = 0; i < buttonCount; i++) {
const button = buttons.nth(i);
const ariaLabel = await button.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
expect(ariaLabel!.length).toBeGreaterThan(0);
}
}
// Check ARIA roles
const landmarks = page.locator('[role]');
const landmarkCount = await landmarks.count();
if (landmarkCount > 0) {
const validRoles = ['navigation', 'main', 'complementary', 'contentinfo', 'banner'];
for (let i = 0; i < landmarkCount; i++) {
const landmark = landmarks.nth(i);
const role = await landmark.getAttribute('role');
expect(validRoles).toContain(role);
}
}
});
test('should handle color contrast consistently', async ({ page }) => {
await page.goto('/accessibility-test');
// Test color contrast using browser DevTools
const contrastResults = await page.evaluate(() => {
const results: any[] = [];
const elements = document.querySelectorAll('[data-contrast-test]');
elements.forEach(element => {
const styles = window.getComputedStyle(element);
const color = styles.color;
const backgroundColor = styles.backgroundColor;
results.push({
element: element.tagName,
color,
backgroundColor
});
});
return results;
});
// In a real test, you'd use a contrast calculation library
expect(contrastResults.length).toBeGreaterThan(0);
contrastResults.forEach(result => {
expect(result.color).not.toBe('rgba(0, 0, 0, 0)');
expect(result.backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
});
});
});
// 8. Cross-browser debugging utilities
// tests/utils/CrossBrowserHelper.ts
import { Page } from '@playwright/test';
export class CrossBrowserHelper {
constructor(private page: Page) {}
async getBrowserInfo() {
return {
userAgent: await this.page.evaluate(() => navigator.userAgent),
viewport: this.page.viewportSize(),
browserName: this.page.context().browser()?.browserType().name()
};
}
async captureConsoleLogs() {
const logs: string[] = [];
this.page.on('console', msg => {
logs.push(`[${msg.type()}] ${msg.text()}`);
});
return logs;
}
async captureNetworkRequests() {
const requests: any[] = [];
this.page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
headers: request.headers()
});
});
return requests;
}
async capturePerformanceMetrics() {
return await this.page.evaluate(() => {
if ('performance' in window) {
return {
navigation: performance.getEntriesByType('navigation')[0],
resources: performance.getEntriesByType('resource'),
timing: performance.timing
};
}
return null;
});
}
async debugLayoutIssues() {
return await this.page.evaluate(() => {
const issues: string[] = [];
// Check for overflow issues
const elements = document.querySelectorAll('*');
elements.forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.right > window.innerWidth || rect.bottom > window.innerHeight) {
issues.push(`Element ${el.tagName}#${el.id || el.className} overflows viewport`);
}
});
// Check for overlapping elements
const visibleElements = Array.from(elements).filter(el => {
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
});
for (let i = 0; i < visibleElements.length; i++) {
for (let j = i + 1; j < visibleElements.length; j++) {
const rect1 = visibleElements[i].getBoundingClientRect();
const rect2 = visibleElements[j].getBoundingClientRect();
if (this.overlaps(rect1, rect2)) {
issues.push(`Potential overlap: ${visibleElements[i].tagName} and ${visibleElements[j].tagName}`);
}
}
}
return issues;
});
}
private overlaps(rect1: DOMRect, rect2: DOMRect): boolean {
return !(rect1.right < rect2.left ||
rect1.left > rect2.right ||
rect1.bottom < rect2.top ||
rect1.top > rect2.bottom);
}
async generateCrossBrowserReport() {
const browserInfo = await this.getBrowserInfo();
const performanceMetrics = await this.capturePerformanceMetrics();
const layoutIssues = await this.debugLayoutIssues();
return {
browser: browserInfo,
performance: performanceMetrics,
layoutIssues,
timestamp: new Date().toISOString()
};
}
}
💻 Tests d'API et Interceptation Réseau text
Tests d'API avancés avec Playwright incluant mocking de requête/réponse, tests d'API REST, support GraphQL et stratégies de tests de performance
// API Testing and Network Interception with Playwright
// 1. Basic API Testing Setup
// tests/api/api.config.ts
import { defineConfig } from '@playwright/test';
const playwrightAdvancedConfig = defineConfig({
testDir: './tests/api',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['json', { outputFile: 'test-results/api-test-results.json' }],
['html', { outputFolder: 'playwright-report-api' }]
],
use: {
baseURL: process.env.API_BASE_URL || 'http://localhost:4000/api',
extraHTTPHeaders: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
trace: 'on-first-retry'
},
projects: [
{
name: 'api-tests',
testMatch: '**/*.api.spec.ts'
}
]
});
// 2. REST API Testing
// tests/api/rest-api.spec.ts
import { test, expect, request } from '@playwright/test';
test.describe('REST API Testing', () => {
const apiBase = process.env.API_BASE_URL || 'http://localhost:4000/api';
let authHeaders: Record<string, string>;
test.beforeAll(async () => {
// Setup authentication headers
const loginResponse = await request.post(`${apiBase}/auth/login`, {
data: {
username: 'testuser',
password: 'testpass'
}
});
expect(loginResponse.ok()).toBeTruthy();
const { token } = await loginResponse.json();
authHeaders = {
'Authorization': `Bearer ${token}`
};
});
test.describe('User Management API', () => {
test('should create a new user', async ({ request }) => {
const newUser = {
username: 'newuser',
email: '[email protected]',
password: 'password123',
role: 'user'
};
const response = await request.post(`${apiBase}/users`, {
data: newUser,
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(201);
const createdUser = await response.json();
expect(createdUser).toMatchObject({
username: newUser.username,
email: newUser.email,
role: newUser.role
});
expect(createdUser).toHaveProperty('id');
expect(createdUser).toHaveProperty('createdAt');
});
test('should get user by ID', async ({ request }) => {
const response = await request.get(`${apiBase}/users/1`, {
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const user = await response.json();
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('username');
expect(user).toHaveProperty('email');
});
test('should update user information', async ({ request }) => {
const updateData = {
email: '[email protected]',
role: 'admin'
};
const response = await request.put(`${apiBase}/users/1`, {
data: updateData,
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const updatedUser = await response.json();
expect(updatedUser).toMatchObject(updateData);
});
test('should delete a user', async ({ request }) => {
const response = await request.delete(`${apiBase}/users/2`, {
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(204);
// Verify deletion
const verifyResponse = await request.get(`${apiBase}/users/2`, {
headers: authHeaders
});
expect(verifyResponse.status()).toBe(404);
});
test('should handle validation errors', async ({ request }) => {
const invalidUser = {
username: '', // Invalid: empty username
email: 'invalid-email', // Invalid: malformed email
password: '123' // Invalid: too short
};
const response = await request.post(`${apiBase}/users`, {
data: invalidUser,
headers: authHeaders
});
expect(response.status()).toBe(400);
const errorResponse = await response.json();
expect(errorResponse).toHaveProperty('error');
expect(errorResponse).toHaveProperty('details');
// Check for specific validation messages
const details = errorResponse.details;
expect(details.some((detail: any) => detail.field === 'username')).toBeTruthy();
expect(details.some((detail: any) => detail.field === 'email')).toBeTruthy();
expect(details.some((detail: any) => detail.field === 'password')).toBeTruthy();
});
});
test.describe('Product Management API', () => {
test('should get all products with pagination', async ({ request }) => {
const response = await request.get(`${apiBase}/products?page=1&limit=10`, {
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(200);
const productsResponse = await response.json();
expect(productsResponse).toHaveProperty('products');
expect(productsResponse).toHaveProperty('pagination');
expect(productsResponse.products).toBeInstanceOf(Array);
expect(productsResponse.products.length).toBeLessThanOrEqual(10);
const pagination = productsResponse.pagination;
expect(pagination).toMatchObject({
page: 1,
limit: 10,
total: expect.any(Number),
totalPages: expect.any(Number)
});
});
test('should search products', async ({ request }) => {
const searchTerm = 'laptop';
const response = await request.get(`${apiBase}/products/search?q=${searchTerm}`, {
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
const searchResults = await response.json();
expect(searchResults).toHaveProperty('products');
// All results should contain the search term
searchResults.products.forEach((product: any) => {
expect(
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase())
).toBeTruthy();
});
});
test('should filter products by category', async ({ request }) => {
const category = 'electronics';
const response = await request.get(`${apiBase}/products?category=${category}`, {
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
const filteredProducts = await response.json();
expect(filteredProducts.products).toBeInstanceOf(Array);
filteredProducts.products.forEach((product: any) => {
expect(product.category).toBe(category);
});
});
});
test.describe('Error Handling', () => {
test('should handle 404 errors', async ({ request }) => {
const response = await request.get(`${apiBase}/nonexistent`, {
headers: authHeaders
});
expect(response.status()).toBe(404);
const errorResponse = await response.json();
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toContain('Not Found');
});
test('should handle 401 unauthorized errors', async ({ request }) => {
const response = await request.get(`${apiBase}/users/protected`, {
headers: {} // No auth headers
});
expect(response.status()).toBe(401);
const errorResponse = await response.json();
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toContain('Unauthorized');
});
test('should handle rate limiting', async ({ request }) => {
const responses = [];
// Make multiple requests quickly to trigger rate limiting
for (let i = 0; i < 10; i++) {
const response = await request.get(`${apiBase}/users`, {
headers: authHeaders
});
responses.push(response);
}
// Check if any response was rate limited
const rateLimitedResponse = responses.find(res => res.status() === 429);
if (rateLimitedResponse) {
const errorResponse = await rateLimitedResponse.json();
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toContain('Rate limit exceeded');
}
});
});
});
// 3. GraphQL API Testing
// tests/api/graphql-api.spec.ts
import { test, expect, request } from '@playwright/test';
test.describe('GraphQL API Testing', () => {
const graphqlEndpoint = process.env.GRAPHQL_ENDPOINT || 'http://localhost:4000/graphql';
let authHeaders: Record<string, string>;
test.beforeAll(async () => {
// Setup authentication
const loginResponse = await request.post(graphqlEndpoint, {
data: {
query: `
mutation Login($username: String!, $password: String!) {
login(username: $username, password: $password) {
token
user {
id
username
role
}
}
}
`,
variables: {
username: 'testuser',
password: 'testpass'
}
}
});
expect(loginResponse.ok()).toBeTruthy();
const { data, errors } = await loginResponse.json();
expect(errors).toBeUndefined();
expect(data).toHaveProperty('login.token');
const { token } = data.login;
authHeaders = {
'Authorization': `Bearer ${token}`
};
});
test('should execute GraphQL query', async ({ request }) => {
const query = {
query: `
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
username
email
role
createdAt
posts {
id
title
publishedAt
}
}
}
`,
variables: {
limit: 10,
offset: 0
}
};
const response = await request.post(graphqlEndpoint, {
data: query,
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
const result = await response.json();
expect(result).toHaveProperty('data');
expect(result).not.toHaveProperty('errors');
const { users } = result.data;
expect(users).toBeInstanceOf(Array);
expect(users.length).toBeLessThanOrEqual(10);
if (users.length > 0) {
const user = users[0];
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('username');
expect(user).toHaveProperty('email');
expect(user).toHaveProperty('role');
expect(user).toHaveProperty('posts');
expect(user.posts).toBeInstanceOf(Array);
}
});
test('should execute GraphQL mutation', async ({ request }) => {
const mutation = {
query: `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
username
email
role
createdAt
}
}
`,
variables: {
input: {
username: 'graphqluser',
email: '[email protected]',
password: 'password123',
role: 'user'
}
}
};
const response = await request.post(graphqlEndpoint, {
data: mutation,
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
const result = await response.json();
expect(result).toHaveProperty('data');
expect(result).not.toHaveProperty('errors');
const { createUser } = result.data;
expect(createUser).toMatchObject({
username: 'graphqluser',
email: '[email protected]',
role: 'user'
});
expect(createUser).toHaveProperty('id');
expect(createUser).toHaveProperty('createdAt');
});
test('should handle GraphQL errors', async ({ request }) => {
const invalidQuery = {
query: `
query GetInvalidField {
users {
invalidField
}
}
`
};
const response = await request.post(graphqlEndpoint, {
data: invalidQuery,
headers: authHeaders
});
expect(response.ok()).toBeTruthy();
const result = await response.json();
expect(result).toHaveProperty('errors');
expect(result.errors).toBeInstanceOf(Array);
expect(result.errors[0]).toHaveProperty('message');
expect(result.errors[0].message).toContain('invalidField');
});
test('should handle GraphQL subscriptions (WebSocket)', async ({ page }) => {
await page.goto('/graphql-subscription-test');
// Test subscription via WebSocket
const subscriptionResult = await page.evaluate(async (endpoint: string) => {
return new Promise((resolve) => {
const ws = new WebSocket(endpoint.replace('http', 'ws'));
ws.onopen = () => {
// Send subscription query
ws.send(JSON.stringify({
type: 'start',
payload: {
query: `
subscription OnUserCreated {
userCreated {
id
username
email
createdAt
}
}
`
}
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'data') {
resolve(data.payload.data.userCreated);
}
};
});
}, graphqlEndpoint);
// Trigger user creation to test subscription
const mutation = {
query: `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
username
}
}
`,
variables: {
input: {
username: 'subscriptiontest',
email: '[email protected]',
password: 'password123',
role: 'user'
}
}
};
await request.post(graphqlEndpoint, {
data: mutation,
headers: authHeaders
});
// Wait for subscription result
expect(subscriptionResult).toBeTruthy();
expect(subscriptionResult.username).toBe('subscriptiontest');
});
});
// 4. Network Interception and Mocking
// tests/api/network-interception.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Network Interception', () => {
test('should intercept and mock API responses', async ({ page }) => {
await page.goto('/dashboard');
// Intercept GET users request
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Mock User 1', email: '[email protected]' },
{ id: 2, name: 'Mock User 2', email: '[email protected]' }
])
});
});
// Load users
await page.click('[data-testid=load-users-button]');
// Verify mocked data is displayed
await expect(page.locator('[data-testid=user-list]')).toContainText('Mock User 1');
await expect(page.locator('[data-testid=user-list]')).toContainText('Mock User 2');
});
test('should simulate API failures', async ({ page }) => {
await page.goto('/dashboard');
// Intercept API call and return error
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' })
});
});
await page.click('[data-testid=load-users-button]');
// Verify error handling
await expect(page.locator('[data-testid=error-message]')).toBeVisible();
await expect(page.locator('[data-testid=error-message]')).toContainText('Failed to load users');
});
test('should modify API requests', async ({ page }) => {
await page.goto('/profile');
// Intercept and modify request
await page.route('**/api/users/profile', async (route) => {
const request = route.request();
const postData = request.postDataJSON();
// Add authentication header
postData.authToken = 'mock-auth-token';
// Continue with modified request
await route.continue({
postData: JSON.stringify(postData)
});
});
// Mock the response
await page.route('**/api/users/profile', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: 'Test User',
email: '[email protected]',
preferences: {
theme: 'dark',
notifications: true
}
})
});
});
await page.click('[data-testid=save-profile-button]');
await expect(page.locator('[data-testid=success-message]')).toBeVisible();
});
test('should handle network delays', async ({ page }) => {
await page.goto('/slow-loading-page');
// Intercept with delay
await page.route('**/api/slow-data', async (route) => {
await new Promise(resolve => setTimeout(resolve, 2000)); // 2 second delay
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: 'slow response data' })
});
});
// Show loading indicator
await expect(page.locator('[data-testid=loading-spinner]')).toBeVisible();
// Click to load slow data
await page.click('[data-testid=load-slow-data]');
// Wait for loading to complete
await expect(page.locator('[data-testid=loading-spinner]')).not.toBeVisible();
await expect(page.locator('[data-testid=data-display]')).toContainText('slow response data');
});
});
// 5. File Upload API Testing
// tests/api/file-upload.spec.ts
import { test, expect, request } from '@playwright/test';
test.describe('File Upload API Testing', () => {
const apiBase = process.env.API_BASE_URL || 'http://localhost:4000/api';
test('should upload single file', async ({ request }) => {
// Create a test file buffer
const fileContent = 'This is a test file content';
const fileBuffer = Buffer.from(fileContent);
const formData = new FormData();
formData.append('file', fileBuffer, 'test.txt');
formData.append('description', 'Test file upload');
const response = await request.post(`${apiBase}/upload`, {
multipart: {
file: {
name: 'test.txt',
mimeType: 'text/plain',
buffer: fileBuffer
},
description: 'Test file upload'
}
});
expect(response.ok()).toBeTruthy();
expect(response.status()).toBe(201);
const result = await response.json();
expect(result).toMatchObject({
filename: 'test.txt',
size: fileContent.length,
mimeType: 'text/plain',
description: 'Test file upload'
});
expect(result).toHaveProperty('id');
expect(result).toHaveProperty('url');
});
test('should upload multiple files', async ({ request }) => {
const files = [
{ name: 'file1.txt', content: 'First file content', mimeType: 'text/plain' },
{ name: 'file2.json', content: '{"key": "value"}', mimeType: 'application/json' }
];
const formData = new FormData();
files.forEach(file => {
const fileBuffer = Buffer.from(file.content);
formData.append('files', fileBuffer, file.name);
});
const response = await request.post(`${apiBase}/upload/multiple`, {
multipart: {
files: files.map(file => ({
name: file.name,
mimeType: file.mimeType,
buffer: Buffer.from(file.content)
}))
}
});
expect(response.ok()).toBeTruthy();
const result = await response.json();
expect(result).toHaveProperty('files');
expect(result.files).toHaveLength(2);
result.files.forEach((uploadedFile: any, index: number) => {
expect(uploadedFile.filename).toBe(files[index].name);
expect(uploadedFile.mimeType).toBe(files[index].mimeType);
});
});
test('should validate file upload constraints', async ({ request }) => {
// Test file size limit
const largeFileContent = 'x'.repeat(11 * 1024 * 1024); // 11MB file
const largeFileBuffer = Buffer.from(largeFileContent);
const response = await request.post(`${apiBase}/upload`, {
multipart: {
file: {
name: 'large.txt',
mimeType: 'text/plain',
buffer: largeFileBuffer
}
}
});
expect(response.status()).toBe(400);
const errorResponse = await response.json();
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toContain('File size too large');
});
});
// 6. API Performance Testing
// tests/api/api-performance.spec.ts
import { test, expect, request } from '@playwright/test';
test.describe('API Performance Testing', () => {
const apiBase = process.env.API_BASE_URL || 'http://localhost:4000/api';
test('should measure API response times', async ({ request }) => {
const endpoints = [
'/users',
'/products',
'/orders'
];
const performanceResults = [];
for (const endpoint of endpoints) {
const startTime = Date.now();
const response = await request.get(`${apiBase}${endpoint}`);
const endTime = Date.now();
const responseTime = endTime - startTime;
performanceResults.push({
endpoint,
responseTime,
status: response.status(),
contentLength: (await response.body()).length
});
expect(responseTime).toBeLessThan(2000); // Should respond within 2 seconds
}
console.table(performanceResults);
// Average response time should be reasonable
const avgResponseTime = performanceResults.reduce(
(sum, result) => sum + result.responseTime, 0
) / performanceResults.length;
expect(avgResponseTime).toBeLessThan(1000);
});
test('should handle concurrent requests', async ({ request }) => {
const numRequests = 20;
const endpoint = `${apiBase}/products`;
// Make concurrent requests
const promises = Array.from({ length: numRequests }, () =>
request.get(endpoint)
);
const startTime = Date.now();
const responses = await Promise.all(promises);
const endTime = Date.now();
const totalTime = endTime - startTime;
// All requests should succeed
responses.forEach(response => {
expect(response.ok()).toBeTruthy();
});
console.log(`${numRequests} concurrent requests completed in ${totalTime}ms`);
// Should complete within reasonable time
expect(totalTime).toBeLessThan(5000);
// Calculate average response time
const avgResponseTime = totalTime / numRequests;
expect(avgResponseTime).toBeLessThan(250);
});
test('should test API load handling', async ({ request }) => {
const loadTestDuration = 10000; // 10 seconds
const requestInterval = 100; // Request every 100ms
const endpoint = `${apiBase}/users`;
const results = [];
const startTime = Date.now();
while (Date.now() - startTime < loadTestDuration) {
const requestStartTime = Date.now();
try {
const response = await request.get(endpoint);
const requestEndTime = Date.now();
const responseTime = requestEndTime - requestStartTime;
results.push({
success: true,
responseTime,
status: response.status()
});
} catch (error) {
results.push({
success: false,
error: error.message
});
}
// Wait before next request
await new Promise(resolve => setTimeout(resolve, requestInterval));
}
const successfulRequests = results.filter(r => r.success);
const failedRequests = results.filter(r => !r.success);
console.log(`Load test completed:`);
console.log(` Total requests: ${results.length}`);
console.log(` Successful: ${successfulRequests.length}`);
console.log(` Failed: ${failedRequests.length}`);
// Success rate should be high
const successRate = (successfulRequests.length / results.length) * 100;
expect(successRate).toBeGreaterThan(95);
// Average response time should be reasonable
if (successfulRequests.length > 0) {
const avgResponseTime = successfulRequests.reduce(
(sum, r) => sum + r.responseTime, 0
) / successfulRequests.length;
expect(avgResponseTime).toBeLessThan(1000);
console.log(` Average response time: ${avgResponseTime.toFixed(2)}ms`);
}
});
});
// 7. API Testing Utilities
// tests/utils/ApiTestHelper.ts
import { APIRequestContext, APIResponse } from '@playwright/test';
export class ApiTestHelper {
constructor(private request: APIRequestContext) {}
async login(username: string, password: string): Promise<{ token: string; user: any }> {
const response = await this.request.post('/auth/login', {
data: { username, password }
});
if (!response.ok()) {
throw new Error(`Login failed: ${response.status()}`);
}
const result = await response.json();
return result;
}
async createTestUser(userData: Partial<any> = {}): Promise<any> {
const defaultUserData = {
username: `testuser_${Date.now()}`,
email: `test${Date.now()}@example.com`,
password: 'testpass123',
role: 'user',
...userData
};
const response = await this.request.post('/users', {
data: defaultUserData
});
if (!response.ok()) {
throw new Error(`User creation failed: ${response.status()}`);
}
return await response.json();
}
async cleanupTestUser(userId: number): Promise<void> {
const response = await this.request.delete(`/users/${userId}`);
// Don't throw error if user doesn't exist
if (response.status() !== 404 && !response.ok()) {
throw new Error(`User cleanup failed: ${response.status()}`);
}
}
async measureApiPerformance(endpoint: string, method = 'GET', data?: any): Promise<PerformanceResult> {
const startTime = performance.now();
let response: APIResponse;
if (method === 'GET') {
response = await this.request.get(endpoint);
} else if (method === 'POST') {
response = await this.request.post(endpoint, { data });
} else if (method === 'PUT') {
response = await this.request.put(endpoint, { data });
} else if (method === 'DELETE') {
response = await this.request.delete(endpoint);
} else {
throw new Error(`Unsupported HTTP method: ${method}`);
}
const endTime = performance.now();
const responseTime = endTime - startTime;
return {
endpoint,
method,
responseTime,
status: response.status(),
success: response.ok(),
contentLength: (await response.body()).length
};
}
async validateApiResponse(response: APIResponse, expectedStatus = 200): Promise<void> {
expect(response.status()).toBe(expectedStatus);
if (expectedStatus === 200 || expectedStatus === 201) {
const contentType = response.headers()['content-type'];
expect(contentType).toContain('application/json');
}
}
async setupTestData(): Promise<void> {
// Create test data required for API tests
await this.createTestUser({ role: 'admin' });
await this.createTestUser({ role: 'moderator' });
}
async cleanupTestData(): Promise<void> {
// Clean up test data
// This would depend on your specific API cleanup requirements
console.log('Cleaning up test data...');
}
}
interface PerformanceResult {
endpoint: string;
method: string;
responseTime: number;
status: number;
success: boolean;
contentLength: number;
}
// 8. API Test Fixtures
// tests/fixtures/api.fixture.ts
import { test as base, expect } from '@playwright/test';
import { ApiTestHelper } from '../utils/ApiTestHelper';
type ApiFixtures = {
apiHelper: ApiTestHelper;
authenticatedRequest: {
request: APIRequestContext;
authToken: string;
};
};
export const test = base.extend<ApiFixtures>({
apiHelper: async ({ request }, use) => {
const helper = new ApiTestHelper(request);
await use(helper);
},
authenticatedRequest: async ({ request, apiHelper }, use) => {
const { token } = await apiHelper.login('testuser', 'testpass');
// Create authenticated request context
const authRequest = request;
await use({ request: authRequest, authToken: token });
}
});
export { expect };
💻 Patterns Avancés de Tests et Meilleures Pratiques text
Stratégies sophistiquées de tests incluant Modèles d'Objet Page, gestion des données de test, rapports, intégration CI/CD et workflows de tests d'entreprise
// Advanced Testing Patterns and Best Practices with Playwright
// 1. Advanced Page Object Model with Factory Pattern
// tests/pages/PageFactory.ts
import { Page } from '@playwright/test';
import { HomePage } from './HomePage';
import { LoginPage } from './LoginPage';
import { DashboardPage } from './DashboardPage';
import { ProductPage } from './ProductPage';
export class PageFactory {
private page: Page;
constructor(page: Page) {
this.page = page;
}
// Factory methods for creating page objects
getHomePage(): HomePage {
return new HomePage(this.page);
}
getLoginPage(): LoginPage {
return new LoginPage(this.page);
}
getDashboardPage(): DashboardPage {
return new DashboardPage(this.page);
}
getProductPage(): ProductPage {
return new ProductPage(this.page);
}
// Context-aware page creation
async getPageFor<T extends PageObject>(pageClass: new (page: Page) => T): Promise<T> {
const pageObject = new pageClass(this.page) as T;
// Wait for page-specific elements
if (pageObject.getIdentifier) {
await this.page.waitForSelector(pageObject.getIdentifier());
}
return pageObject;
}
}
// tests/pages/BasePage.ts - Enhanced base page
import { Page, Locator } from '@playwright/test';
import { TestDataLoader } from '../utils/TestDataLoader';
import { WaitHelper } from '../utils/WaitHelper';
export abstract class BasePage {
protected page: Page;
protected testDataLoader: TestDataLoader;
protected waitHelper: WaitHelper;
constructor(page: Page) {
this.page = page;
this.testDataLoader = new TestDataLoader();
this.waitHelper = new WaitHelper(page);
}
// Abstract method that child classes must implement
abstract getIdentifier(): string;
// Common locators that might be present on many pages
protected readonly header = this.page.locator('header');
protected readonly footer = this.page.locator('footer');
protected readonly mainNavigation = this.page.locator('[data-testid=main-nav]');
protected readonly userMenu = this.page.locator('[data-testid=user-menu]');
protected readonly notifications = this.page.locator('[data-testid=notifications]');
protected readonly loadingIndicator = this.page.locator('[data-testid=loading]');
// Enhanced navigation methods
async navigateTo(path: string, options?: { waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' }): Promise<void> {
await this.page.goto(path, {
waitUntil: options?.waitUntil || 'domcontentloaded',
timeout: 30000
});
// Wait for page identifier
await this.waitHelper.waitForElement(this.getIdentifier());
}
// Enhanced wait methods
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
await this.waitHelper.waitForElement(this.getIdentifier());
}
async waitForContentToLoad(selector: string): Promise<void> {
await this.waitHelper.waitForElement(selector);
}
async waitForElementToDisappear(selector: string): Promise<void> {
await this.waitHelper.waitForElementToDisappear(selector);
}
// Screenshot and debugging utilities
async takeScreenshot(name: string, options?: { fullPage?: boolean }): Promise<void> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `screenshots/${name}_${timestamp}.png`;
await this.page.screenshot({
path: fileName,
fullPage: options?.fullPage || true
});
}
async takeScreenshotOfElement(selector: string, name: string): Promise<void> {
const element = this.page.locator(selector);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `screenshots/${name}_${timestamp}.png`;
await element.screenshot({ path: fileName });
}
// Accessibility testing
async checkAccessibility(): Promise<void> {
// Integration with axe-core or similar accessibility testing
const violations = await this.page.evaluate(() => {
// This would integrate with axe-core
return []; // Return accessibility violations
});
if (violations.length > 0) {
console.warn('Accessibility violations found:', violations);
}
}
// Form interaction utilities
async fillForm(formData: Record<string, string>): Promise<void> {
for (const [field, value] of Object.entries(formData)) {
const element = this.page.locator(`[data-testid="${field}"], [name="${field}"], #${field}`);
await element.fill(value);
}
}
async fillFormWithValidation(formData: Record<string, string>): Promise<void> {
await this.fillForm(formData);
// Wait for validation to complete
await this.page.waitForTimeout(500);
// Check for validation errors
const errorElements = this.page.locator('[data-testid*="error"], .error, .invalid-feedback');
const errorCount = await errorElements.count();
if (errorCount > 0) {
const errors: string[] = [];
for (let i = 0; i < errorCount; i++) {
const errorText = await errorElements.nth(i).textContent();
if (errorText) errors.push(errorText);
}
throw new Error(`Form validation failed: ${errors.join(', ')}`);
}
}
// Toast/notification utilities
async waitForNotification(message?: string, timeout = 5000): Promise<Locator> {
const notification = this.page.locator('[data-testid="notification"]');
await notification.waitFor({ state: 'visible', timeout });
if (message) {
await expect(notification).toContainText(message);
}
return notification;
}
async waitForNotificationToDisappear(): Promise<void> {
const notification = this.page.locator('[data-testid="notification"]');
await notification.waitFor({ state: 'hidden', timeout: 5000 });
}
// Performance monitoring
async measurePageLoadTime(): Promise<number> {
const navigationEntry = await this.page.evaluate(() => {
return performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
});
return navigationEntry.loadEventEnd - navigationEntry.loadEventStart;
}
// Data testing utilities
async verifyTableData(expectedData: Record<string, string>[]): Promise<void> {
const tableRows = this.page.locator('[data-testid="table-row"]');
const rowCount = await tableRows.count();
expect(rowCount).toBe(expectedData.length);
for (let i = 0; i < expectedData.length; i++) {
const rowData = expectedData[i];
const row = tableRows.nth(i);
for (const [key, value] of Object.entries(rowData)) {
const cell = row.locator(`[data-testid="cell-${key}"]`);
await expect(cell).toContainText(value);
}
}
}
// API interaction within page context
async waitForApiCall(apiPattern: string, timeout = 30000): Promise<any> {
return await this.page.waitForResponse(response =>
response.url().includes(apiPattern),
{ timeout }
);
}
async captureApiResponse(apiPattern: string): Promise<any[]> {
const responses: any[] = [];
this.page.on('response', response => {
if (response.url().includes(apiPattern)) {
responses.push(response);
}
});
return responses;
}
// Responsive testing utilities
async testResponsive(viewports: Array<{ name: string; width: number; height: number }>): Promise<void> {
for (const viewport of viewports) {
await this.page.setViewportSize({ width: viewport.width, height: viewport.height });
// Wait for layout to stabilize
await this.page.waitForTimeout(1000);
// Take screenshot for visual comparison
await this.takeScreenshot(`responsive-${viewport.name}`);
// Verify key elements are visible and properly positioned
await this.verifyResponsiveLayout(viewport);
}
}
protected async verifyResponsiveLayout(viewport: { width: number; height: number }): Promise<void> {
// Override in child classes for specific layout verification
}
// Error handling utilities
async retryOperation<T>(
operation: () => Promise<T>,
maxAttempts = 3,
delay = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
console.warn(`Operation failed (attempt ${attempt}/${maxAttempts}):`, error);
if (attempt < maxAttempts) {
await this.page.waitForTimeout(delay);
}
}
}
throw lastError!;
}
}
// tests/pages/HomePage.ts - Enhanced home page
import { BasePage } from './BasePage';
export class HomePage extends BasePage {
// Enhanced locators with better selectors
readonly heroSection = this.page.locator('[data-testid="hero-section"]');
readonly heroTitle = this.page.locator('[data-testid="hero-title"]');
readonly heroSubtitle = this.page.locator('[data-testid="hero-subtitle"]');
readonly getStartedButton = this.page.getByRole('button', { name: /get started/i });
readonly featuredProducts = this.page.locator('[data-testid="featured-products"]');
readonly productCards = this.page.locator('[data-testid="product-card"]');
readonly testimonials = this.page.locator('[data-testid="testimonials"]');
readonly newsletterSection = this.page.locator('[data-testid="newsletter-section"]');
getIdentifier(): string {
return '[data-testid="home-page"]';
}
// Enhanced page interactions
async visit(): Promise<void> {
await this.navigateTo('/');
await this.waitForPageLoad();
await this.takeScreenshot('homepage-loaded');
}
async verifyHeroSection(): Promise<void> {
await this.waitHelper.waitForElement(this.heroSection);
await expect(this.heroTitle).toBeVisible();
await expect(this.heroSubtitle).toBeVisible();
await expect(this.getStartedButton).toBeVisible();
// Verify accessibility
await this.checkAccessibility();
}
async navigateToGetStarted(): Promise<void> {
await this.waitHelper.waitForElement(this.getStartedButton);
await this.retryOperation(async () => {
await this.getStartedButton.click();
});
}
async verifyFeaturedProducts(expectedCount?: number): Promise<void> {
await this.waitHelper.waitForElement(this.featuredProducts);
if (expectedCount) {
await expect(this.productCards).toHaveCount(expectedCount);
}
// Verify each product card has required elements
const productCount = await this.productCards.count();
for (let i = 0; i < productCount; i++) {
const card = this.productCards.nth(i);
await expect(card.locator('[data-testid="product-title"]')).toBeVisible();
await expect(card.locator('[data-testid="product-price"]')).toBeVisible();
await expect(card.locator('[data-testid="product-image"]')).toBeVisible();
}
}
async clickProductCard(productName: string): Promise<void> {
const productCard = this.productCards.filter({ hasText: productName }).first();
await this.waitHelper.waitForElement(productCard);
await productCard.click();
}
async subscribeToNewsletter(email: string): Promise<void> {
await this.waitHelper.waitForElement(this.newsletterSection);
const emailInput = this.page.locator('[data-testid="newsletter-email"]');
const subscribeButton = this.page.getByRole('button', { name: /subscribe/i });
await this.retryOperation(async () => {
await emailInput.fill(email);
await subscribeButton.click();
});
await this.waitForNotification('Successfully subscribed to newsletter');
}
// Advanced features search
async searchFeatures(searchTerm: string): Promise<void> {
const searchInput = this.page.locator('[data-testid="features-search"]');
const searchButton = this.page.getByRole('button', { name: /search/i });
await searchInput.fill(searchTerm);
await searchButton.click();
// Wait for search results
await this.page.waitForSelector('[data-testid="search-results"]');
}
// Performance testing
async measureLoadPerformance(): Promise<PerformanceMetrics> {
await this.visit();
const loadTime = await this.measurePageLoadTime();
const lighthouseScore = await this.runLighthouseAudit();
return {
loadTime,
lighthouseScore,
timestamp: new Date().toISOString()
};
}
private async runLighthouseAudit(): Promise<number> {
// Integration with lighthouse
return 95; // Placeholder for actual lighthouse score
}
}
interface PerformanceMetrics {
loadTime: number;
lighthouseScore: number;
timestamp: string;
}
// 2. Advanced Test Data Management
// tests/utils/TestDataManager.ts
import { randomBytes } from 'crypto';
export class TestDataManager {
private testData: Map<string, any> = new Map();
// Generate unique test data
generateUser(overrides: Partial<any> = {}): any {
const timestamp = Date.now();
const randomString = randomBytes(4).toString('hex');
return {
username: `testuser_${timestamp}_${randomString}`,
email: `test_${timestamp}_${randomString}@example.com`,
firstName: 'Test',
lastName: 'User',
password: `password_${randomString}`,
role: 'user',
createdAt: new Date().toISOString(),
...overrides
};
}
generateProduct(overrides: Partial<any> = {}): any {
const timestamp = Date.now();
const randomString = randomBytes(4).toString('hex');
return {
name: `Test Product ${timestamp}_${randomString}`,
description: 'This is a test product',
price: Math.floor(Math.random() * 1000) + 1,
category: 'electronics',
sku: `SKU-${timestamp}-${randomString}`,
inStock: true,
createdAt: new Date().toISOString(),
...overrides
};
}
generateOrder(userId: string, overrides: Partial<any> = {}): any {
const timestamp = Date.now();
return {
userId,
items: this.generateOrderItems(),
total: Math.floor(Math.random() * 500) + 50,
status: 'pending',
orderNumber: `ORD-${timestamp}`,
createdAt: new Date().toISOString(),
...overrides
};
}
private generateOrderItems(count = 2): any[] {
const items = [];
for (let i = 0; i < count; i++) {
items.push({
productId: Math.floor(Math.random() * 100) + 1,
quantity: Math.floor(Math.random() * 5) + 1,
price: Math.floor(Math.random() * 200) + 10
});
}
return items;
}
// Store and retrieve test data
storeData(key: string, data: any): void {
this.testData.set(key, data);
}
getData(key: string): any {
return this.testData.get(key);
}
// Cleanup utilities
async cleanupTestData(apiHelper: any): Promise<void> {
const testDataArray = Array.from(this.testData.values());
// Clean up users
const users = testDataArray.filter(data => data.username);
for (const user of users) {
try {
await apiHelper.deleteUser(user.id);
} catch (error) {
console.warn('Failed to cleanup user:', error);
}
}
// Clean up products
const products = testDataArray.filter(data => data.sku);
for (const product of products) {
try {
await apiHelper.deleteProduct(product.id);
} catch (error) {
console.warn('Failed to cleanup product:', error);
}
}
// Clear test data cache
this.testData.clear();
}
}
// 3. Advanced Wait Helper
// tests/utils/WaitHelper.ts
import { Page, Locator } from '@playwright/test';
export class WaitHelper {
constructor(private page: Page) {}
async waitForElement(selector: string, options: { timeout?: number; state?: 'visible' | 'hidden' } = {}): Promise<void> {
const { timeout = 10000, state = 'visible' } = options;
await this.page.waitForSelector(selector, {
timeout,
state
});
}
async waitForElementToDisappear(selector: string, timeout = 10000): Promise<void> {
await this.waitForElement(selector, { timeout, state: 'hidden' });
}
async waitForText(selector: string, text: string, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await element.waitFor({ state: 'visible', timeout });
await expect(element).toContainText(text, { timeout });
}
async waitForUrl(urlPattern: string, timeout = 10000): Promise<void> {
await this.page.waitForURL(urlPattern, { timeout });
}
async waitForNetworkIdle(timeout = 30000): Promise<void> {
await this.page.waitForLoadState('networkidle', { timeout });
}
async waitForElementToBeEnabled(selector: string, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await element.waitFor({ state: 'visible', timeout });
await expect(element).toBeEnabled({ timeout });
}
async waitForElementToBeDisabled(selector: string, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await element.waitFor({ state: 'visible', timeout });
await expect(element).toBeDisabled({ timeout });
}
async waitForElementToHaveCount(selector: string, count: number, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await expect(element).toHaveCount(count, { timeout });
}
async waitForElementToHaveAttribute(selector: string, attribute: string, value: string, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await expect(element).toHaveAttribute(attribute, value, { timeout });
}
async waitForElementToHaveClass(selector: string, className: string, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await expect(element).toHaveClass(className, { timeout });
}
async waitForElementToBeVisible(selector: string, timeout = 10000): Promise<void> {
await this.waitForElement(selector, { timeout, state: 'visible' });
}
async waitForPageLoadComplete(timeout = 30000): Promise<void> {
await Promise.all([
this.page.waitForLoadState('domcontentloaded', { timeout }),
this.page.waitForLoadState('load', { timeout })
]);
}
async waitForAnimationsToComplete(timeout = 5000): Promise<void> {
await this.page.waitForFunction(() => {
return document.getAnimations().length === 0;
}, { timeout });
}
async waitForCustomCondition(condition: () => boolean, timeout = 10000, message = 'Custom condition not met'): Promise<void> {
await this.page.waitForFunction(
(cond) => cond(),
condition,
{ timeout }
).catch(() => {
throw new Error(`${message} within ${timeout}ms`);
});
}
async waitForStableElement(selector: string, stableTime = 1000, timeout = 10000): Promise<void> {
const element = this.page.locator(selector);
await element.waitFor({ state: 'visible', timeout });
let previousPosition: any = null;
const startTime = Date.now();
while (Date.now() - startTime < stableTime) {
const currentPosition = await element.boundingBox();
if (previousPosition) {
const isStable =
Math.abs(currentPosition!.x - previousPosition.x) < 1 &&
Math.abs(currentPosition!.y - previousPosition.y) < 1;
if (isStable) {
break;
}
}
previousPosition = currentPosition;
await this.page.waitForTimeout(100);
}
}
}
// 4. Advanced Test Utilities
// tests/utils/TestUtils.ts
import { Page, Locator } from '@playwright/test';
export class TestUtils {
constructor(private page: Page) {}
// Random data generation
static generateRandomString(length = 8): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
static generateRandomEmail(): string {
return `test${Date.now()}@${this.generateRandomString(6)}.com`;
}
static generateRandomPhoneNumber(): string {
return `${Math.floor(Math.random() * 9000000000) + 1000000000}`;
}
// Form utilities
async fillFormWithRandomData(fieldSelectors: string[]): Promise<Record<string, string>> {
const formData: Record<string, string> = {};
for (const selector of fieldSelectors) {
const fieldType = await this.page.evaluate((sel) => {
const element = document.querySelector(sel) as HTMLInputElement;
return element?.type || 'text';
}, selector);
let value: string;
switch (fieldType) {
case 'email':
value = TestUtils.generateRandomEmail();
break;
case 'tel':
value = TestUtils.generateRandomPhoneNumber();
break;
case 'password':
value = TestUtils.generateRandomString(12);
break;
default:
value = TestUtils.generateRandomString(10);
}
await this.page.fill(selector, value);
formData[selector] = value;
}
return formData;
}
// Validation utilities
async isElementVisible(selector: string): Promise<boolean> {
try {
const element = this.page.locator(selector);
await element.waitFor({ state: 'visible', timeout: 1000 });
return true;
} catch {
return false;
}
}
async getElementText(selector: string): Promise<string> {
const element = this.page.locator(selector);
return await element.textContent() || '';
}
async getElementAttribute(selector: string, attribute: string): Promise<string | null> {
const element = this.page.locator(selector);
return await element.getAttribute(attribute);
}
// Navigation utilities
async getCurrentUrl(): Promise<string> {
return this.page.url();
}
async waitForNavigation(callback: () => Promise<void>): Promise<void> {
await Promise.all([
this.page.waitForNavigation(),
callback()
]);
}
// Cookie and storage utilities
async setCookie(name: string, value: string): Promise<void> {
await this.page.context().addCookies([{
name,
value,
url: this.page.url(),
domain: new URL(this.page.url()).hostname
}]);
}
async getCookie(name: string): Promise<string | null> {
const cookies = await this.page.context().cookies();
const cookie = cookies.find(c => c.name === name);
return cookie?.value || null;
}
async setLocalStorage(key: string, value: string): Promise<void> {
await this.page.evaluate(([k, v]) => {
localStorage.setItem(k, v);
}, [key, value]);
}
async getLocalStorage(key: string): Promise<string | null> {
return await this.page.evaluate((k) => {
return localStorage.getItem(k);
}, [key]);
}
// Screenshot utilities
async takeElementScreenshot(selector: string, fileName: string): Promise<void> {
const element = this.page.locator(selector);
await element.screenshot({ path: `screenshots/${fileName}.png` });
}
async takeFullPageScreenshot(fileName: string): Promise<void> {
await this.page.screenshot({
path: `screenshots/${fileName}.png`,
fullPage: true
});
}
// Performance utilities
async measurePerformance(callback: () => Promise<void>): Promise<number> {
const startTime = performance.now();
await callback();
const endTime = performance.now();
return endTime - startTime;
}
async getPerformanceMetrics(): Promise<any> {
return await this.page.evaluate(() => {
return {
navigation: performance.getEntriesByType('navigation')[0],
resources: performance.getEntriesByType('resource'),
timing: performance.timing,
memory: (performance as any).memory
};
});
}
}
// 5. Advanced Test Configuration
// tests/config/test-environments.ts
export interface TestEnvironment {
name: string;
baseUrl: string;
apiBaseUrl: string;
databaseUrl: string;
features: string[];
}
export const testEnvironments: Record<string, TestEnvironment> = {
development: {
name: 'Development',
baseUrl: 'http://localhost:3000',
apiBaseUrl: 'http://localhost:4000/api',
databaseUrl: 'mongodb://localhost:27017/testdb',
features: ['debug-mode', 'mock-payments', 'fast-loading']
},
staging: {
name: 'Staging',
baseUrl: 'https://staging.example.com',
apiBaseUrl: 'https://api-staging.example.com/api',
databaseUrl: 'mongodb://staging-db:27017/stagingdb',
features: ['analytics', 'performance-monitoring']
},
production: {
name: 'Production',
baseUrl: 'https://example.com',
apiBaseUrl: 'https://api.example.com/api',
databaseUrl: 'mongodb://prod-db:27017/proddb',
features: ['full-features', 'monitoring', 'backups']
}
};
// 6. Test Reporting and Analytics
// tests/utils/TestReporter.ts
export class TestReporter {
private testResults: any[] = [];
addTestResult(result: any): void {
this.testResults.push({
...result,
timestamp: new Date().toISOString()
});
}
generateTestReport(): TestReport {
const passed = this.testResults.filter(r => r.status === 'passed').length;
const failed = this.testResults.filter(r => r.status === 'failed').length;
const skipped = this.testResults.filter(r => r.status === 'skipped').length;
return {
summary: {
total: this.testResults.length,
passed,
failed,
skipped,
passRate: (passed / this.testResults.length) * 100
},
details: this.testResults,
generatedAt: new Date().toISOString()
};
}
async generatePerformanceReport(): Promise<PerformanceReport> {
// This would integrate with performance data collected during tests
return {
averageLoadTime: 1500,
slowestPage: 'checkout',
fastestPage: 'home',
recommendations: [
'Optimize images on product pages',
'Implement lazy loading for testimonials'
],
generatedAt: new Date().toISOString()
};
}
}
interface TestReport {
summary: {
total: number;
passed: number;
failed: number;
skipped: number;
passRate: number;
};
details: any[];
generatedAt: string;
}
interface PerformanceReport {
averageLoadTime: number;
slowestPage: string;
fastestPage: string;
recommendations: string[];
generatedAt: string;
}
// 7. Example Advanced Test
// tests/advanced/user-journey.spec.ts
import { test, expect } from '../fixtures/base.fixture';
import { TestDataManager } from '../utils/TestDataManager';
import { TestUtils } from '../utils/TestUtils';
test.describe('Complete User Journey', () => {
let testDataManager: TestDataManager;
let testUser: any;
let testUtils: TestUtils;
test.beforeAll(async ({ page }) => {
testDataManager = new TestDataManager();
testUtils = new TestUtils(page);
testUser = testDataManager.generateUser();
});
test.beforeEach(async ({ page }) => {
testUtils = new TestUtils(page);
});
test('complete user registration and first purchase journey', async ({ page, loginPage, homePage, apiHelper }) => {
// Step 1: Visit homepage
await homePage.visit();
await homePage.verifyHeroSection();
// Step 2: Navigate to registration
await homePage.navigateToGetStarted();
await page.waitForURL('**/register');
// Step 3: Fill registration form
const registrationForm = page.locator('[data-testid="registration-form"]');
await testUtils.fillFormWithRandomData([
'[data-testid="first-name"]',
'[data-testid="last-name"]',
'[data-testid="email"]',
'[data-testid="password"]',
'[data-testid="confirm-password"]'
]);
// Complete registration
await page.click('[data-testid="register-button"]');
await testUtils.waitForNotification('Registration successful');
// Step 4: Login with new credentials
await loginPage.visit();
await loginPage.login(testUser.username, testUser.password);
await loginPage.verifySuccessfulLogin();
// Step 5: Browse products
await homePage.visit();
await homePage.verifyFeaturedProducts();
// Search for specific product
await homePage.searchFeatures('laptop');
await homePage.clickProductCard('Premium Laptop');
// Step 6: Add product to cart
const productPage = page.locator('[data-testid="product-page"]');
await testUtils.waitForElement('[data-testid="add-to-cart-button"]');
await page.click('[data-testid="add-to-cart-button"]');
await testUtils.waitForNotification('Product added to cart');
// Step 7: View cart and checkout
await page.click('[data-testid="cart-button"]');
await page.waitForURL('**/cart');
await page.click('[data-testid="checkout-button"]');
await page.waitForURL('**/checkout');
// Step 8: Fill shipping information
const checkoutForm = page.locator('[data-testid="checkout-form"]');
await checkoutForm.fill({
'[data-testid="shipping-address"]': '123 Test Street',
'[data-testid="shipping-city"]': 'Test City',
'[data-testid="shipping-zip"]': '12345'
});
// Step 9: Complete purchase
await page.click('[data-testid="place-order-button"]');
await testUtils.waitForNotification('Order placed successfully');
// Step 10: Verify order confirmation
await expect(page.locator('[data-testid="order-confirmation"]')).toBeVisible();
const orderNumber = await testUtils.getElementText('[data-testid="order-number"]');
expect(orderNumber).toMatch(/^ORD-d+$/);
// Step 11: Verify order in user dashboard
await page.click('[data-testid="dashboard-button"]');
await page.waitForURL('**/dashboard');
await page.click('[data-testid="orders-tab"]');
const ordersList = page.locator('[data-testid="orders-list"]');
await expect(ordersList).toContainText(orderNumber);
});
test('should handle error scenarios gracefully', async ({ page, loginPage }) => {
// Test invalid login
await loginPage.visit();
await loginPage.login('invalid', 'credentials');
await loginPage.getErrorMessage().then(error => {
expect(error).toContain('Invalid credentials');
});
// Test network error handling
await page.route('**/api/**', route => route.abort());
await page.goto('/');
await testUtils.waitForElement('[data-testid="network-error"]', { timeout: 5000 });
});
});