mcpField
P
playwright-testing

playwright-testing

github.com/fcakyon测试与质量1.2k39 次安装1 天前更新
代码仓库

关于 playwright-testing

为编写 Playwright 测试的开发者整理测试组织、定位器、认证、文件操作、网络模拟和调试等实践建议。内容还介绍减少不稳定测试的方法,以及通过 responsive-tester 检查响应式布局。

主要功能

  1. 01按功能组织测试文件与命名
  2. 02使用语义化定位器和 Page Object
  3. 03复用认证状态并测试多用户
  4. 04处理文件上传与下载
  5. 05模拟网络请求并调试失败测试
  6. 06检查响应式断点与布局

适用场景

  1. 01为登录和仪表盘功能组织端到端测试
  2. 02复用登录状态测试不同权限用户
  3. 03模拟 API 响应验证界面行为
  4. 04排查竞态条件和不稳定用例
  5. 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");
});

定位器策略

优先级顺序(从优到劣)
  1. getByRole - 易于访问且稳定
  2. getByLabel - 表单输入项
  3. getByPlaceholder - 没有标签时使用
  4. getByText - 可见文本
  5. getByTestId - 没有更好的选择时使用
  6. 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