Playwright测试Skill playwright-testing

这个技能提供了使用Playwright进行Web自动化测试的最佳实践指南,涵盖测试组织、页面对象模型、定位器策略、认证处理、文件上传、等待策略、网络模拟、CI/CD集成、调试技巧和修复不稳定测试。关键词:Playwright,自动化测试,Web测试,UI测试,端到端测试,测试最佳实践。

测试 0 次安装 0 次浏览 更新于 3/16/2026

name: playwright-testing description: 当用户询问 “Playwright”、“响应式测试”、“使用 Playwright 测试”、“测试登录流程”、“文件上传测试”、“处理测试中的认证” 或 “修复不稳定测试” 时,应使用此技能。

Playwright 测试最佳实践

测试组织

文件结构

tests/
├── auth/
│   ├── login.spec.ts
│   └── signup.spec.ts
├── dashboard/
│   └── dashboard.spec.ts
├── fixtures/
│   └── test-data.ts
├── pages/
│   └── login.page.ts
└── playwright.config.ts

命名约定

  • 文件: 功能名称.spec.ts
  • 测试: 描述用户行为,而非实现细节
  • 好例子: test('用户可以通过邮箱重置密码')
  • 坏例子: test('测试重置密码')

页面对象模型

基本模式

// 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("邮箱").fill(email);
    await this.page.getByLabel("密码").fill(password);
    await this.page.getByRole("button", { name: "登录" }).click();
  }
}

// tests/login.spec.ts
test("成功登录", async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login("user@example.com", "password");
  await expect(page).toHaveURL("/dashboard");
});

定位器策略

优先级顺序(从优到劣)

  1. getByRole - 可访问性强,健壮
  2. getByLabel - 表单输入
  3. getByPlaceholder - 无标签时使用
  4. getByText - 可见文本
  5. getByTestId - 无更好选项时使用
  6. CSS/XPath - 最后手段

示例

// 首选
await page.getByRole("button", { name: "提交" }).click();
await page.getByLabel("邮箱地址").fill("user@example.com");

// 可接受
await page.getByTestId("submit-button").click();

// 避免
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("邮箱").fill(process.env.TEST_USER_EMAIL);
  await page.getByLabel("密码").fill(process.env.TEST_USER_PASSWORD);
  await page.getByRole("button", { name: "登录" }).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",
  },
});

多用户场景

// 创建不同认证状态
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";

test.describe("管理员功能", () => {
  test.use({ storageState: adminAuth });
  // 管理员测试
});

test.describe("用户功能", () => {
  test.use({ storageState: userAuth });
  // 用户测试
});

文件上传处理

基本上传

// 单个文件
await page.getByLabel("上传文件").setInputFiles("path/to/file.pdf");

// 多个文件
await page
  .getByLabel("上传文件")
  .setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);

// 清除文件输入
await page.getByLabel("上传文件").setInputFiles([]);

拖放上传

// 从缓冲区创建文件
const buffer = Buffer.from("文件内容");

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: "下载" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());

等待策略

自动等待(首选)

Playwright 自动等待元素。使用断言:

// 自动等待元素可见且稳定
await page.getByRole("button", { name: "提交" }).click();

// 自动等待条件
await expect(page.getByText("成功")).toBeVisible();

显式等待(需要时)

// 等待导航
await page.waitForURL("**/dashboard");

// 等待网络空闲
await page.waitForLoadState("networkidle");

// 等待特定响应
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: "测试用户" }]),
  });
});

// 模拟错误响应
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: 运行 Playwright 测试
  run: npx playwright test
  env:
    CI: true

- name: 上传测试结果
  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,
});

调试失败测试

调试工具

# 使用 UI 模式运行
npx playwright test --ui

# 使用检查器运行
npx playwright test --debug

# 显示浏览器
npx playwright test --headed

跟踪查看器

// playwright.config.ts
use: {
  trace: 'on-first-retry', // 失败时捕获跟踪
}

修复不稳定测试

常见原因和解决方案

竞态条件:

  • 使用适当断言而非硬等待
  • 等待网络请求完成

动画问题:

  • 在测试配置中禁用动画
  • 等待动画完成

动态内容:

  • 使用灵活定位器(文本内容而非位置)
  • 等待加载状态解析

测试隔离:

  • 每个测试应设置自身状态
  • 不依赖其他测试的副作用

应避免的反模式

// 坏: 硬等待
await page.waitForTimeout(5000);

// 好: 等待条件
await expect(page.getByText("已加载")).toBeVisible();

// 坏: 不稳定选择器
await page.locator(".btn:nth-child(3)").click();

// 好: 语义选择器
await page.getByRole("button", { name: "提交" }).click();

响应式设计测试

要进行全面的响应式设计测试,可使用 responsive-tester 代理。它自动:

  • 在 7 个标准断点(375px 至 1536px)测试页面
  • 检测水平溢出问题
  • 验证移动优先设计模式
  • 检查触摸目标大小(最小 44x44px)
  • 标记反模式,如无移动后备的固定宽度

通过询问 “测试响应式”、“检查断点” 或 “测试移动/桌面布局” 来触发。