🎯 Рекомендуемые коллекции
Балансированные коллекции примеров кода из различных категорий, которые вы можете исследовать
Playwright E2E Тестовый Фреймворк
Всесторонние примеры тестов Playwright включая кросс-браузерную автоматизацию, тесты API, мобильные тесты, визуальную регрессию и продвинутые E2E паттерны для современных веб приложений
💻 Настройка и Конфигурация Проекта Playwright typescript
Полная настройка проекта Playwright с конфигурацией TypeScript, структурой тестов, фикстурами и лучшими практиками для надежных E2E тестов
// Playwright Project Setup and Configuration
// 1. playwright.config.ts - Main configuration
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
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'],
['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 default 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 default 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();
});
});
💻 Кросс-браузерные и Мультиплатформенные Тесты typescript
Всесторонние примеры кросс-браузерных тестов включая мобильные тесты, проверку адаптивного дизайна и специфичные для платформы стратегии тестирования
// 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';
export default 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()
};
}
}
💻 Тесты API и Перехват Сетевых Запросов typescript
Продвинутые тесты API с Playwright включая мокинг запросов/ответов, тесты REST API, поддержку GraphQL и стратегии тестирования производительности
// API Testing and Network Interception with Playwright
// 1. Basic API Testing Setup
// tests/api/api.config.ts
import { defineConfig } from '@playwright/test';
export default 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 };
💻 Продвинутые Паттерны Тестирования и Лучшие Практики typescript
Софистированные стратегии тестирования включая Page Object Models, управление тестовыми данными, отчетность, CI/CD интеграцию и корпоративные рабочие процессы тестирования
// 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 });
});
});