关于 playwright
面向编写浏览器端端到端测试的 Playwright 技能,涵盖测试配置、定位器、操作与断言,也介绍视觉回归和 CI 等实践。
主要功能
- 01浏览器测试配置
- 02定位器、操作与断言
- 03Page Object 模式
- 04复用身份验证状态
- 05视觉回归与 CI 指引
适用场景
- 01编写浏览器用户流程测试
- 02检查页面元素与交互
- 03在测试间复用登录状态
- 04通过截图审查视觉变化
MCP 相关
该技能指向 `mcp__documentation__fetch_docs`,用于获取完整的 Playwright 文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill playwright安装后在 Claude Code 中输入/playwright确认已加载。
附带文件quick-ref/locators.mdquick-ref/page-objects.md
详细介绍
Playwright 核心知识
深入了解:使用
mcp__documentation__fetch_docs并指定技术:playwright,获取完整文档。
何时不应使用此技能
- 单元测试 - 使用
vitest或jest测试独立的函数/模块 - 组件测试 - 使用
testing-library测试 React/Vue 组件 - 仅 API 测试 - 使用框架专用的 HTTP 客户端或 REST 客户端
- 性能测试 - 使用 k6 或 Lighthouse 等专用工具
- 移动应用测试 - 使用 Appium 或 Detox 测试原生移动应用
基本测试
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', '[email protected]');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Welcome');
});
定位器
// By role (preferred)
page.getByRole('button', { name: 'Submit' });
page.getByRole('link', { name: 'Home' });
page.getByLabel('Email');
page.getByPlaceholder('Enter email');
page.getByText('Welcome');
// By test id
page.getByTestId('submit-button');
// CSS/XPath (fallback)
page.locator('.submit-btn');
page.locator('#user-form');
断言
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
// Text
await expect(locator).toHaveText('Hello');
await expect(locator).toContainText('Hello');
// Attributes
await expect(locator).toHaveAttribute('href', '/home');
await expect(locator).toHaveClass(/active/);
// Input
await expect(locator).toHaveValue('[email protected]');
// Page
await expect(page).toHaveURL(/dashboard/);
await expect(page).toHaveTitle('Dashboard');
操作
await page.click('button');
await page.fill('input', 'text');
await page.check('input[type="checkbox"]');
await page.selectOption('select', 'option1');
await page.hover('.menu-item');
await page.keyboard.press('Enter');
页面对象
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.fill('[name="email"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
}
配置
// playwright.config.ts
export default defineConfig({
testDir: './e2e',
use: { baseURL: 'http://localhost:3000' },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }
]
});
生产就绪
测试配置
// 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'],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
身份验证状态
// Save auth state for reuse
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
await page.waitForURL('/dashboard');
// Save storage state
await page.context().storageState({ path: '.auth/user.json' });
});
// Use in tests
import { test } from '@playwright/test';
test.use({ storageState: '.auth/user.json' });
test('authenticated test', async ({ page }) => {
await page.goto('/dashboard');
// Already logged in
});
API 测试
test('API test', async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'John', email: '[email protected]' },
headers: { Authorization: `Bearer ${token}` },
});
expect(response.ok()).toBeTruthy();
const user = await response.json();
expect(user.name).toBe('John');
});
// Mock API responses
test('with mocked API', async ({ page }) => {
await page.route('/api/users', async route => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Mock User' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Mock User')).toBeVisible();
});
视觉回归
test('visual regression', async ({ page }) => {
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100,
threshold: 0.2,
});
});
// Update snapshots: npx playwright test --update-snapshots
CI 配置
# GitHub Actions
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: ${{ secrets.STAGING_URL }}
- name: Upload report
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
监控指标
| 指标 | 目标 |
|---|---|
| 端到端测试通过率 | > 99% |
| 测试执行时间 | < 10min |
| 不稳定测试率 | < 1% |
| 视觉差异失败 | 全部审核 |
检查清单
- 已配置多浏览器测试
- 移动端视口测试
- 复用身份验证状态
- 失败时重试(仅 CI)
- 失败时截图
- 已启用跟踪收集
- 使用 API 模拟实现隔离
- 视觉回归测试
- CI/CD 集成
- 测试并行化
- 使用页面对象模式
反模式
| 反模式 | 缺点 | 解决方案 |
|---|---|---|
| 通过 CSS 选择器测试 | 脆弱,样式变化时会失效 | 使用 getByRole、getByLabel、getByTestId |
| 未等待元素出现 | 测试不稳定 | 使用自动等待定位器,避免使用 waitForTimeout |
| 硬编码等待(page.waitForTimeout) | 速度慢、不可靠 | 使用 page.waitForSelector 或自动等待 |
| 未使用页面对象模型 | 代码重复,难以维护 | 将常用操作提取到页面对象中 |
| 在单个测试中测试过多内容 | 难以调试失败 | 每个测试覆盖一个用户流程 |
| 未复用身份验证状态 | 每个测试都要重新登录,速度慢 | 保存 storageState,并在测试间复用 |
| 忽略不稳定测试 | 造成虚假的信心 | 修复不稳定测试,谨慎使用重试 |
快速故障排除
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| “等待选择器超时” | 元素尚未渲染或选择器错误 | 检查选择器,确保元素存在 |
| 测试不稳定(随机通过/失败) | 竞态条件、网络缓慢 | 使用自动等待,避免使用 waitForTimeout |
| “元素不可见” | 元素被隐藏或不在视口内 | 滚动至元素可见处,检查 CSS display |
| “执行上下文已销毁” | 操作期间发生了导航 | 等待导航完成 |
| 截图不匹配 | 字体渲染、动画 | 禁用动画,使用固定视口 |
| 测试一直挂起 | 缺少 await | 确保所有异步调用都使用 await |
参考文档
评论
支持 Markdown