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
- Playwright Official Site
- Playwright GitHub Repository
- Playwright Official Documentation
- Playwright Python API
- Playwright Java API
- Playwright .NET API
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