playwright
4
总安装量
2
周安装量
#49305
全站排名
安装命令
npx skills add https://github.com/fractionestate/midnight-dev-skills --skill playwright
Agent 安装分布
antigravity
2
gemini-cli
2
replit
1
opencode
1
kimi-cli
1
Skill 文档
Playwright E2E Testing
Playwright is a modern end-to-end testing framework that supports Chromium, Firefox, and WebKit. It provides auto-wait, network interception, and powerful debugging tools.
Core Concepts
Project Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html', { open: 'never' }], ['list'], process.env.CI ? ['github'] : ['line']],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
Basic Test Structure
// e2e/homepage.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Homepage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('has correct title', async ({ page }) => {
await expect(page).toHaveTitle(/My App/);
});
test('navigation works', async ({ page }) => {
await page.getByRole('link', { name: 'About' }).click();
await expect(page).toHaveURL('/about');
});
test('search functionality', async ({ page }) => {
await page.getByPlaceholder('Search...').fill('test query');
await page.getByRole('button', { name: 'Search' }).click();
await expect(page.getByTestId('results')).toBeVisible();
});
});
Locator Strategies
Best Practices (Priority Order)
// 1. Role-based (best accessibility)
page.getByRole('button', { name: 'Submit' });
page.getByRole('heading', { level: 1 });
page.getByRole('link', { name: /learn more/i });
// 2. Label-based (forms)
page.getByLabel('Email');
page.getByPlaceholder('Enter your email');
// 3. Text-based (visible text)
page.getByText('Welcome');
page.getByText(/sign up/i);
// 4. Test ID (when others don't work)
page.getByTestId('user-avatar');
// 5. CSS/XPath (last resort)
page.locator('.card:has-text("Featured")');
page.locator('//button[contains(@class, "primary")]');
Filtering Locators
// Filter by text
page.getByRole('listitem').filter({ hasText: 'Product' });
// Filter by another locator
page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'Buy' }),
});
// Chain locators
page.getByRole('article').getByRole('button');
// Nth element
page.getByRole('listitem').nth(2);
page.getByRole('listitem').first();
page.getByRole('listitem').last();
Actions & Assertions
Common Actions
// Click
await page.getByRole('button').click();
await page.getByRole('button').dblclick();
await page.getByRole('button').click({ button: 'right' });
// Type
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Email').pressSequentially('user@example.com');
await page.keyboard.press('Enter');
// Select
await page.getByLabel('Country').selectOption('USA');
await page.getByLabel('Colors').selectOption(['red', 'blue']);
// Checkbox/Radio
await page.getByLabel('Agree').check();
await page.getByLabel('Agree').uncheck();
// Upload
await page.getByLabel('Upload').setInputFiles('file.pdf');
await page.getByLabel('Upload').setInputFiles(['file1.pdf', 'file2.pdf']);
// Drag and drop
await page.locator('#source').dragTo(page.locator('#target'));
Assertions
// Element state
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeFocused();
// Content
await expect(locator).toHaveText('Hello');
await expect(locator).toContainText('Hello');
await expect(locator).toHaveValue('input value');
await expect(locator).toHaveAttribute('href', '/about');
await expect(locator).toHaveClass(/active/);
await expect(locator).toHaveCSS('color', 'rgb(0, 0, 0)');
// Page
await expect(page).toHaveTitle(/Home/);
await expect(page).toHaveURL('/dashboard');
// Count
await expect(locator).toHaveCount(3);
// Screenshot comparison
await expect(page).toHaveScreenshot('homepage.png');
await expect(locator).toHaveScreenshot('button.png');
Page Object Model
// e2e/pages/login.page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toHaveText(message);
}
}
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password');
await expect(page).toHaveURL('/dashboard');
});
Fixtures
// e2e/fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/login.page';
type Fixtures = {
loginPage: LoginPage;
authenticatedPage: void;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
authenticatedPage: async ({ page }, use) => {
// Set auth cookies
await page
.context()
.addCookies([{ name: 'session', value: 'test-session', domain: 'localhost', path: '/' }]);
await use();
},
});
export { expect };
// Usage
test('authenticated test', async ({ page, authenticatedPage }) => {
await page.goto('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
});
API Testing
import { test, expect } from '@playwright/test';
test.describe('API Tests', () => {
test('GET users', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
const users = await response.json();
expect(users).toHaveLength(3);
});
test('POST create user', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'John', email: 'john@example.com' },
});
expect(response.status()).toBe(201);
});
});
Network Interception
test('mock API response', async ({ page }) => {
await page.route('/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mocked User' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Mocked User')).toBeVisible();
});
test('intercept and modify', async ({ page }) => {
await page.route('/api/**', async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});
});
Visual Regression Testing
test('visual comparison', async ({ page }) => {
await page.goto('/');
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
// Component screenshot
const card = page.getByTestId('feature-card');
await expect(card).toHaveScreenshot('feature-card.png');
});
Debugging
# Run in headed mode
npx playwright test --headed
# Run with UI mode
npx playwright test --ui
# Debug specific test
npx playwright test --debug
# Show HTML report
npx playwright show-report
Best Practices
- Use role-based locators for accessibility and stability
- Page Object Model for maintainable tests
- Fixtures for shared setup and authentication
- Auto-wait – avoid explicit waits when possible
- Isolate tests – each test should be independent
- CI parallelization – run tests in parallel for speed
References
- references/selectors.md – Selector patterns
- references/fixtures.md – Fixtures and setup