Testing Tool

Playwright

Overview

Playwright is a cross-browser automation library developed by Microsoft that supports Chromium, Firefox, and WebKit as a modern E2E testing framework. Through true cross-browser testing, fast and stable test execution, and rich API functionality, it enables reliable integration testing and E2E testing for modern web application development. With multi-language support for JavaScript, TypeScript, Python, C#, and Java, along with headless/full display modes, mobile emulation, and parallel execution capabilities, it provides a comprehensive browser automation solution that meets the testing requirements of any development team.

Details

Playwright 2025 edition has established itself as cutting-edge technology in web application testing. Developed directly by Microsoft's browser engine development team, it fully supports the three major browser engines: Chromium (Google Chrome, Microsoft Edge), Firefox, and WebKit (Safari). Compared to traditional Selenium-based tools, it achieves over 10x faster performance and significantly improves test reliability and stability. Through auto-wait functionality, complete element visibility confirmation, detailed network response control, and request/response interception capabilities, it fundamentally solves flaky tests. With integrated API Testing, Visual Testing, and component testing features, it provides one-stop coverage from E2E testing to unit testing. It excels in DevOps pipeline integration, powerfully supporting continuous test automation through headless execution in CI/CD environments, detailed test report generation, and screenshot/video recording functionality.

Key Features

  • True Cross-browser Support: Complete support for Chromium, Firefox, and WebKit engines
  • Fast and Stable Test Execution: Improved reliability through auto-wait and smart element detection
  • Multi-language Support: JavaScript/TypeScript, Python, C#, and Java compatibility
  • Integrated API Testing: Unified workflow for UI testing and API testing
  • Visual Testing: Screenshot comparison and regression detection
  • Parallel Execution and CI/CD Integration: High-speed test execution and continuous integration optimization

Pros and Cons

Pros

  • High technical capability and continuous browser engine updates from Microsoft development
  • Significant reduction in flaky tests and stable test execution through auto-wait functionality
  • True cross-browser testing with complete support for three browser engines
  • Multi-language support for JavaScript, Python, C#, Java with rich ecosystem
  • Integrated solution for API Testing, Visual Testing, and Component Testing
  • High-speed test execution and DevOps integration through parallel execution and CI/CD optimization
  • Detailed test reports, screenshots, and video recording capabilities
  • Excellent debugging features and real-time test monitoring

Cons

  • Smaller community compared to Selenium as a relatively new tool
  • Increased disk space and memory usage due to three browser engine installations
  • No support for older browser versions or special browsers (IE, etc.)
  • Fewer third-party extensions and plugins compared to Selenium
  • Learning cost and migration cost from existing Selenium tests
  • Configuration adjustments needed for some complex SPAs (Single Page Applications)

Reference Links

Code Examples

Installation and Setup

# JavaScript/TypeScript installation
npm init playwright@latest
# or
npm install -D @playwright/test
npx playwright install

# Python installation
pip install playwright
playwright install

# Java setup (Maven)
# Add dependency to pom.xml
<dependency>
  <groupId>com.microsoft.playwright</groupId>
  <artifactId>playwright</artifactId>
  <version>1.47.0</version>
</dependency>

# C# installation (.NET)
dotnet add package Microsoft.Playwright
dotnet tool install --global Microsoft.Playwright.CLI
playwright install

# Project initialization
npx playwright init
playwright codegen https://example.com  # Auto-generate test code

Basic E2E Testing (JavaScript/TypeScript)

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

test('basic page navigation test', async ({ page }) => {
  // Navigate to page
  await page.goto('https://example.com');
  
  // Check page title
  await expect(page).toHaveTitle(/Example Domain/);
  
  // Check element visibility
  await expect(page.locator('h1')).toHaveText('Example Domain');
  await expect(page.locator('h1')).toBeVisible();
});

test('form input and navigation', async ({ page }) => {
  await page.goto('https://example.com/contact');
  
  // Form input
  await page.fill('input[name="name"]', 'John Doe');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('textarea[name="message"]', 'This is a contact message.');
  
  // Checkbox and radio button
  await page.check('input[type="checkbox"]');
  await page.click('input[value="option1"]');
  
  // Submit button click
  await page.click('button[type="submit"]');
  
  // Check success message
  await expect(page.locator('.success-message')).toHaveText('Message sent successfully');
  
  // Check URL change
  await expect(page).toHaveURL(/.*\/contact\/success/);
});

test('multiple elements test and assertions', async ({ page }) => {
  await page.goto('https://example.com/products');
  
  // Check existence of multiple elements
  const products = page.locator('.product-item');
  await expect(products).toHaveCount(8);
  
  // Check text of each element
  await expect(products.nth(0)).toContainText('Product 1');
  await expect(products.nth(1)).toContainText('Product 2');
  
  // Conditional assertions
  const cartButton = page.locator('.add-to-cart');
  if (await cartButton.isVisible()) {
    await cartButton.click();
    await expect(page.locator('.cart-count')).toHaveText('1');
  }
});

Parallel Testing and Configuration (playwright.config.js)

// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true, // Enable parallel execution
  forbidOnly: !!process.env.CI, // Forbid test.only in CI environment
  retries: process.env.CI ? 2 : 0, // Retry settings for CI environment
  workers: process.env.CI ? 1 : undefined, // Parallel degree adjustment for CI environment
  
  // Report settings
  reporter: [
    ['html'], 
    ['junit', { outputFile: 'test-results/junit.xml' }],
    ['json', { outputFile: 'test-results/results.json' }]
  ],
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry', // Trace recording on failure
    screenshot: 'only-on-failure', // Screenshot on failure
    video: 'retain-on-failure', // Video recording on failure
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
    {
      name: 'Microsoft Edge',
      use: { ...devices['Desktop Edge'], channel: 'msedge' },
    },
  ],

  // Auto-start development server
  webServer: {
    command: 'npm run start',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});

API Testing Integration

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

test.describe('API Testing', () => {
  let apiContext;

  test.beforeAll(async ({ playwright }) => {
    // Create API context
    apiContext = await playwright.request.newContext({
      baseURL: 'https://api.example.com',
      extraHTTPHeaders: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        'Content-Type': 'application/json',
      },
    });
  });

  test.afterAll(async () => {
    await apiContext.dispose();
  });

  test('GET request test', async () => {
    const response = await apiContext.get('/users/123');
    
    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(200);
    
    const user = await response.json();
    expect(user).toMatchObject({
      id: 123,
      name: expect.any(String),
      email: expect.stringContaining('@'),
    });
  });

  test('POST request and data creation', async () => {
    const newUser = {
      name: 'New User',
      email: '[email protected]',
      role: 'user'
    };

    const response = await apiContext.post('/users', {
      data: newUser
    });

    expect(response.ok()).toBeTruthy();
    expect(response.status()).toBe(201);

    const createdUser = await response.json();
    expect(createdUser).toMatchObject(newUser);
    expect(createdUser.id).toBeDefined();
  });

  test('UI operation and API validation combination', async ({ page }) => {
    // Create new item via UI
    await page.goto('/dashboard');
    await page.fill('input[name="title"]', 'New Item');
    await page.click('button[type="submit"]');

    // Check creation success message
    await expect(page.locator('.success')).toBeVisible();

    // Validate created item via API
    const response = await apiContext.get('/items');
    const items = await response.json();
    const createdItem = items.find(item => item.title === 'New Item');
    
    expect(createdItem).toBeDefined();
    expect(createdItem.status).toBe('active');
  });
});

Python Example

# test_example.py
import pytest
from playwright.sync_api import Page, expect

def test_basic_navigation(page: Page):
    """Basic page navigation test"""
    page.goto("https://example.com")
    
    # Check page title
    expect(page).to_have_title("Example Domain")
    
    # Check element visibility
    heading = page.locator("h1")
    expect(heading).to_have_text("Example Domain")
    expect(heading).to_be_visible()

def test_form_interaction(page: Page):
    """Form operation test"""
    page.goto("https://example.com/contact")
    
    # Form input
    page.fill('input[name="name"]', 'John Doe')
    page.fill('input[name="email"]', '[email protected]')
    page.fill('textarea[name="message"]', 'This is a contact message.')
    
    # Submit button click
    page.click('button[type="submit"]')
    
    # Check success message
    success_message = page.locator('.success-message')
    expect(success_message).to_have_text('Message sent successfully')

def test_multiple_browsers(browser_name, page: Page):
    """Multi-browser test"""
    page.goto("https://example.com")
    
    print(f"Testing on {browser_name}")
    
    # Browser-specific behavior check
    if browser_name == "webkit":
        # Safari-specific test
        expect(page.locator("h1")).to_be_visible()
    elif browser_name == "firefox":
        # Firefox-specific test
        expect(page.locator("h1")).to_be_visible()
    else:
        # Chromium-specific test
        expect(page.locator("h1")).to_be_visible()

# conftest.py - Test configuration
import pytest
from playwright.sync_api import sync_playwright

@pytest.fixture(scope="session", params=["chromium", "firefox", "webkit"])
def browser_name(request):
    return request.param

@pytest.fixture(scope="session")
def browser(browser_name):
    with sync_playwright() as p:
        browser_type = getattr(p, browser_name)
        browser = browser_type.launch(headless=True)
        yield browser
        browser.close()

@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page
    context.close()

Java Example

// ExampleTest.java
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import static com.microsoft.playwright.assertions.PlaywrightAssertions.*;

public class ExampleTest {
    static Playwright playwright;
    static Browser browser;
    BrowserContext context;
    Page page;

    @BeforeAll
    static void launchBrowser() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch();
    }

    @AfterAll
    static void closeBrowser() {
        playwright.close();
    }

    @BeforeEach
    void createContextAndPage() {
        context = browser.newContext();
        page = context.newPage();
    }

    @AfterEach
    void closeContext() {
        context.close();
    }

    @Test
    void basicNavigationTest() {
        page.navigate("https://example.com");
        
        // Check page title
        assertThat(page).hasTitle("Example Domain");
        
        // Check element visibility
        Locator heading = page.locator("h1");
        assertThat(heading).hasText("Example Domain");
        assertThat(heading).isVisible();
    }

    @Test
    void formInteractionTest() {
        page.navigate("https://example.com/contact");
        
        // Form input
        page.fill("input[name='name']", "John Doe");
        page.fill("input[name='email']", "[email protected]");
        page.fill("textarea[name='message']", "This is a contact message.");
        
        // Submit button click
        page.click("button[type='submit']");
        
        // Check success message
        Locator successMessage = page.locator(".success-message");
        assertThat(successMessage).hasText("Message sent successfully");
    }

    @Test
    void apiIntegrationTest() {
        // Create API Request context
        APIRequestContext request = playwright.request().newContext(
            new APIRequest.NewContextOptions()
                .setBaseURL("https://api.example.com")
                .setExtraHTTPHeaders(Map.of(
                    "Authorization", "Bearer " + System.getenv("API_TOKEN"),
                    "Content-Type", "application/json"
                ))
        );

        // GET request test
        APIResponse response = request.get("/users/123");
        assertTrue(response.ok());
        assertEquals(200, response.status());

        // POST request test
        Map<String, Object> userData = Map.of(
            "name", "New User",
            "email", "[email protected]"
        );
        
        APIResponse createResponse = request.post("/users", 
            RequestOptions.create().setData(userData));
        assertTrue(createResponse.ok());
        assertEquals(201, createResponse.status());

        request.dispose();
    }
}

Visual Testing (Screenshot Comparison)

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

test('visual regression test', async ({ page }) => {
  await page.goto('https://example.com');
  
  // Full page screenshot comparison
  await expect(page).toHaveScreenshot('homepage.png');
  
  // Specific element screenshot comparison
  await expect(page.locator('.header')).toHaveScreenshot('header.png');
  
  // Full page screenshot
  await expect(page).toHaveScreenshot('homepage-full.png', { fullPage: true });
});

test('responsive design test', async ({ page }) => {
  // Mobile viewport
  await page.setViewportSize({ width: 375, height: 667 });
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('mobile-view.png');
  
  // Tablet viewport
  await page.setViewportSize({ width: 768, height: 1024 });
  await expect(page).toHaveScreenshot('tablet-view.png');
  
  // Desktop viewport
  await page.setViewportSize({ width: 1920, height: 1080 });
  await expect(page).toHaveScreenshot('desktop-view.png');
});

test('dynamic content visual test', async ({ page }) => {
  await page.goto('https://example.com/chart');
  
  // Wait for animation completion
  await page.waitForFunction('() => document.querySelector(".chart").classList.contains("loaded")');
  
  // Screenshot with masking (excluded areas)
  await expect(page).toHaveScreenshot('chart-view.png', {
    mask: [page.locator('.timestamp'), page.locator('.random-id')]
  });
});

CI/CD Integration (GitHub Actions)

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

Debugging and Troubleshooting

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

test('utilizing debug features', async ({ page }) => {
  // Debug execution with browser display: npx playwright test --headed
  // Debug mode: npx playwright test --debug
  
  await page.goto('https://example.com');
  
  // Step-by-step execution
  await page.pause(); // Pause execution
  
  // Output element details
  const element = page.locator('h1');
  console.log('Element text:', await element.textContent());
  console.log('Element HTML:', await element.innerHTML());
  
  // Output page information
  console.log('Current URL:', page.url());
  console.log('Page title:', await page.title());
  
  // Custom wait and timeout
  await page.waitForTimeout(2000); // Wait 2 seconds
  await page.waitForLoadState('networkidle'); // Network wait
  
  // Trace recording (detailed debug information)
  await page.context().tracing.start({ screenshots: true, snapshots: true });
  // Test execution
  await page.click('button');
  await page.context().tracing.stop({ path: 'trace.zip' });
  
  expect(await element.textContent()).toBe('Expected Text');
});

// Using code generator
// npx playwright codegen https://example.com