关于 playwright-testing
为编写 Playwright 测试的开发者整理测试组织、定位器、认证、文件操作、网络模拟和调试等实践建议。内容还介绍减少不稳定测试的方法,以及通过 responsive-tester 检查响应式布局。
主要功能
- 01按功能组织测试文件与命名
- 02使用语义化定位器和 Page Object
- 03复用认证状态并测试多用户
- 04处理文件上传与下载
- 05模拟网络请求并调试失败测试
- 06检查响应式断点与布局
适用场景
- 01为登录和仪表盘功能组织端到端测试
- 02复用登录状态测试不同权限用户
- 03模拟 API 响应验证界面行为
- 04排查竞态条件和不稳定用例
- 05检查页面在移动端与桌面端的布局
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ fcakyon/ claude-codex-settings --skill playwright-testing安装后在 Claude Code 中输入/playwright-testing确认已加载。
详细介绍
Playwright 测试最佳实践
测试组织
文件结构
tests/
├── auth/
│ ├── login.spec.ts
│ └── signup.spec.ts
├── dashboard/
│ └── dashboard.spec.ts
├── fixtures/
│ └── test-data.ts
├── pages/
│ └── login.page.ts
└── playwright.config.ts
命名约定
- 文件:
feature-name.spec.ts - 测试:描述用户行为,而非实现方式
- 好的示例:
test('user can reset password via email') - 不好的示例:
test('test reset password')
页面对象模型
基本模式
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email);
await this.page.getByLabel("Password").fill(password);
await this.page.getByRole("button", { name: "Sign in" }).click();
}
}
// tests/login.spec.ts
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("[email protected]", "password");
await expect(page).toHaveURL("/dashboard");
});
定位器策略
优先级顺序(从优到劣)
getByRole- 易于访问且稳定getByLabel- 表单输入项getByPlaceholder- 没有标签时使用getByText- 可见文本getByTestId- 没有更好的选择时使用- CSS/XPath - 最后的选择
示例
// Preferred
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email address").fill("[email protected]");
// Acceptable
await page.getByTestId("submit-button").click();
// Avoid
await page.locator("#submit-btn").click();
await page.locator('//button[@type="submit"]').click();
身份验证处理
存储状态(推荐)
保存登录状态,并在多个测试中重复使用:
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL);
await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD);
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth.json",
},
});
多用户场景
// Create different auth states
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";
test.describe("admin features", () => {
test.use({ storageState: adminAuth });
// Admin tests
});
test.describe("user features", () => {
test.use({ storageState: userAuth });
// User tests
});
文件上传处理
基本上传
// Single file
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");
// Multiple files
await page
.getByLabel("Upload files")
.setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);
// Clear file input
await page.getByLabel("Upload file").setInputFiles([]);
拖放上传
// Create file from buffer
const buffer = Buffer.from("file content");
await page.getByTestId("dropzone").dispatchEvent("drop", {
dataTransfer: {
files: [{ name: "test.txt", mimeType: "text/plain", buffer }],
},
});
文件下载
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());
等待策略
自动等待(首选)
Playwright 会自动等待元素。请使用断言:
// Auto-waits for element to be visible and stable
await page.getByRole("button", { name: "Submit" }).click();
// Auto-waits for condition
await expect(page.getByText("Success")).toBeVisible();
显式等待(必要时使用)
// Wait for navigation
await page.waitForURL("**/dashboard");
// Wait for network idle
await page.waitForLoadState("networkidle");
// Wait for specific response
await page.waitForResponse((resp) => resp.url().includes("/api/data"));
网络模拟
模拟 API 响应
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Test User" }]),
});
});
// Mock error response
await page.route("**/api/users", async (route) => {
await route.fulfill({ status: 500 });
});
拦截并修改
await page.route("**/api/data", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});
CI/CD 集成
GitHub Actions 示例
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
并行执行
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
});
调试失败的测试
调试工具
# Run with UI mode
npx playwright test --ui
# Run with inspector
npx playwright test --debug
# Show browser
npx playwright test --headed
Trace Viewer
// playwright.config.ts
use: {
trace: 'on-first-retry', // Capture trace on failure
}
修复不稳定的测试
常见原因和解决方案
竞态条件:
- 使用恰当的断言,而不是长时间硬等待
- 等待网络请求完成
动画问题:
- 在测试配置中禁用动画
- 等待动画完成
动态内容:
- 使用灵活的定位器(文本内容,而非位置)
- 等待加载状态结束
测试隔离:
- 每个测试都应设置自己的状态
- 不要依赖其他测试的副作用
应避免的反模式
// Bad: Hard sleep
await page.waitForTimeout(5000);
// Good: Wait for condition
await expect(page.getByText("Loaded")).toBeVisible();
// Bad: Flaky selector
await page.locator(".btn:nth-child(3)").click();
// Good: Semantic selector
await page.getByRole("button", { name: "Submit" }).click();
响应式设计测试
如需全面测试不同视口断点下的响应式布局,请使用 responsive-tester Agent。它会自动:
- 在 7 个标准断点(375px 至 1536px)下测试页面
- 检测水平溢出问题
- 验证移动优先设计模式
- 检查触控目标尺寸(最小为 44x44px)
- 标记未设置移动端回退方案的固定宽度等反模式
提出“测试响应式布局”“检查断点”或“测试移动端/桌面端布局”即可触发它。
评论
支持 Markdown