mcpField
P
playwright

playwright

github.com/gentleman-programming测试与质量661190 次安装31 周前更新
代码仓库

关于 playwright

playwright 是一项用于创建端到端测试的技能,面向使用 Playwright 的测试编写者。它强调先检查页面和实际交互,再编写测试,并规范页面对象复用、选择器和测试文档。

主要功能

  1. 01创建端到端测试
  2. 02先探索页面再编写测试
  3. 03优先使用角色和标签选择器
  4. 04复用页面对象与共享辅助函数
  5. 05按规范组织测试和文档

适用场景

  1. 01为登录或注册流程编写测试
  2. 02核实表单交互与页面跳转
  3. 03为现有页面对象补充测试
  4. 04整理测试辅助函数和用例文档

MCP 相关

若有 Playwright MCP 工具,此技能要求先用其导航、查看页面快照、进行交互并截图,再编写测试;若不可用,则依据文档和代码分析。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gentleman-programming/gentleman-skills --skill playwright

安装后在 Claude Code 中输入/playwright确认已加载。

详细介绍

MCP 工作流(如可用则必须遵循)

⚠️ 如果你有 Playwright MCP 工具,创建任何测试之前都必须先使用这些工具:

  1. 导航到目标页面
  2. 拍摄快照,查看页面结构和元素
  3. 与表单和元素交互,验证确切的用户流程
  4. 截取屏幕截图,记录预期状态
  5. 验证页面跳转,覆盖完整流程(加载、成功、错误)
  6. 记录实际选择器(来自快照,使用真实的引用和标签)
  7. 只有探索完毕后,才能使用已验证的选择器编写测试代码

如果无法使用 MCP: 根据文档和代码分析创建测试。

这样做的重要性:

  • ✅ 测试精准——准确覆盖所需步骤,不做假设
  • ✅ 选择器准确——基于真实 DOM 结构,而非想象
  • ✅ 验证真实流程——确认整个流程确实可用
  • ✅ 避免过度设计——只为现有功能编写必要测试
  • ✅ 避免不稳定的测试——实际探索能让测试更稳定
  • ❌ 切勿假设 UI「应该」如何运行

文件结构

tests/
├── base-page.ts              # Parent class for ALL pages
├── helpers.ts                # Shared utilities
└── {page-name}/
    ├── {page-name}-page.ts   # Page Object Model
    ├── {page-name}.spec.ts   # ALL tests here (NO separate files!)
    └── {page-name}.md        # Test documentation

文件命名:

  • ✅ sign-up.spec.ts(所有注册测试)
  • ✅ sign-up-page.ts(页面对象)
  • ✅ sign-up.md(文档)
  • ❌ sign-up-critical-path.spec.ts(错误——不要创建单独的文件)
  • ❌ sign-up-validation.spec.ts(错误)

选择器优先级(必须遵循)

// 1. BEST - getByRole for interactive elements
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navLink = page.getByRole("link", { name: "Dashboard" });

// 2. BEST - getByLabel for form controls
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");

// 3. SPARINGLY - getByText for static content only
this.errorMessage = page.getByText("Invalid credentials");
this.pageTitle = page.getByText("Welcome");

// 4. LAST RESORT - getByTestId when above fail
this.customWidget = page.getByTestId("date-picker");

// ❌ AVOID fragile selectors
this.button = page.locator(".btn-primary");  // NO
this.input = page.locator("#email");         // NO

范围判断(存在歧义时先询问)

用户的说法操作
「一个测试」「新增一个测试」「新测试」「添加测试」在现有 spec 中创建一个 test()
「全面测试」「所有测试」「测试套件」「生成测试」创建完整测试套件

示例:

  • 「为用户注册创建一个测试」→ 仅创建一个测试
  • 「为登录页面生成端到端测试」→ 创建完整测试套件
  • 「添加一个测试以验证表单验证」→ 在现有 spec 中添加一个测试

页面对象模式

import { Page, Locator, expect } from "@playwright/test";

// BasePage - ALL pages extend this
export class BasePage {
  constructor(protected page: Page) {}

  async goto(path: string): Promise<void> {
    await this.page.goto(path);
    await this.page.waitForLoadState("networkidle");
  }

  // Common methods go here (see Refactoring Guidelines)
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }

  async verifyNotificationMessage(message: string): Promise<void> {
    const notification = this.page.locator('[role="status"]');
    await expect(notification).toContainText(message);
  }
}

// Page-specific implementation
export interface LoginData {
  email: string;
  password: string;
}

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.getByLabel("Email");
    this.passwordInput = page.getByLabel("Password");
    this.submitButton = page.getByRole("button", { name: "Sign in" });
  }

  async goto(): Promise<void> {
    await super.goto("/login");
  }

  async login(data: LoginData): Promise<void> {
    await this.emailInput.fill(data.email);
    await this.passwordInput.fill(data.password);
    await this.submitButton.click();
  }

  async verifyCriticalOutcome(): Promise<void> {
    await expect(this.page).toHaveURL("/dashboard");
  }
}

复用页面对象(至关重要)

创建新的页面对象之前,务必检查现有页面对象!

// ✅ GOOD: Reuse existing page objects
import { SignInPage } from "../sign-in/sign-in-page";
import { HomePage } from "../home/home-page";

test("User can sign up and login", async ({ page }) => {
  const signUpPage = new SignUpPage(page);
  const signInPage = new SignInPage(page);  // REUSE
  const homePage = new HomePage(page);      // REUSE

  await signUpPage.signUp(userData);
  await homePage.verifyPageLoaded();  // REUSE method
  await homePage.signOut();           // REUSE method
  await signInPage.login(credentials); // REUSE method
});

// ❌ BAD: Recreating existing functionality
export class SignUpPage extends BasePage {
  async logout() { /* ... */ }  // ❌ HomePage already has this
  async login() { /* ... */ }   // ❌ SignInPage already has this
}

指南:

  • 首先检查 tests/ 中是否已有页面对象
  • 导入并复用现有页面
  • 仅在页面对象不存在时创建新的
  • 如果测试需要多个页面,请确保所有页面对象都存在(如有需要则创建)

重构指南

在以下情况下移至 BasePage:
  • ✅ 多个页面共用的导航辅助方法(waitForPageLoad()、getCurrentUrl())
  • ✅ 常见 UI 交互(通知、模态框、主题切换)
  • ✅ 多个页面重复使用的验证模式(isVisible()、waitForVisible())
  • ✅ 适用于所有页面的错误处理
  • ✅ 用于调试的屏幕截图工具
在以下情况下移至 helpers.ts:
  • ✅ 测试数据生成(generateUniqueEmail()、generateTestUser())
  • ✅ 设置和清理辅助工具(createTestUser()、cleanupTestData())
  • ✅ 自定义断言(expectNotificationToContain())
  • ✅ 用于测试设置的 API 辅助工具(seedDatabase()、resetState())
  • ✅ 时间辅助工具(waitForCondition()、retryAction())

重构前(不佳):

// Repeated in multiple page objects
export class SignUpPage extends BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }
}
export class SignInPage extends BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');  // DUPLICATED!
  }
}

重构后(良好):

// BasePage - shared across all pages
export class BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }
}

// helpers.ts - data generation
export function generateUniqueEmail(): string {
  return `test.${Date.now()}@example.com`;
}

export function generateTestUser() {
  return {
    name: "Test User",
    email: generateUniqueEmail(),
    password: "TestPassword123!",
  };
}

带标签的测试模式

import { test, expect } from "@playwright/test";
import { LoginPage } from "./login-page";

test.describe("Login", () => {
  test("User can login successfully",
    { tag: ["@critical", "@e2e", "@login", "@LOGIN-E2E-001"] },
    async ({ page }) => {
      const loginPage = new LoginPage(page);

      await loginPage.goto();
      await loginPage.login({ email: "[email protected]", password: "pass123" });

      await expect(page).toHaveURL("/dashboard");
    }
  );
});

标签类别:

  • 优先级:@critical、@high、@medium、@low
  • 类型:@e2e
  • 功能:@signup、@signin、@dashboard
  • 测试 ID:@SIGNUP-E2E-001、@LOGIN-E2E-002

测试文档格式({page-name}.md)

### E2E Tests: {Feature Name}

**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}

---

## Test Case: `{TEST-ID}` - {Test case title}

**Priority:** `{critical|high|medium|low}`

**Tags:**
- type → @e2e
- feature → @{feature-name}

**Description/Objective:** {Brief description}

**Preconditions:**
- {Prerequisites for test to run}
- {Required data or state}

### Flow Steps:
1. {Step 1}
2. {Step 2}
3. {Step 3}

### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}

### Key verification points:
- {Assertion 1}
- {Assertion 2}

### Notes:
- {Additional considerations}

文档规则:

  • ❌ 不要包含通用的测试运行说明
  • ❌ 不要解释文件结构
  • ❌ 不要提供代码示例或教程
  • ❌ 不要包含故障排除章节
  • ✅ 只关注具体的测试用例
  • ✅ 尽可能控制在 60 行以内

命令

npx playwright test                    # Run all
npx playwright test --grep "login"     # Filter by name
npx playwright test --ui               # Interactive UI
npx playwright test --debug            # Debug mode
npx playwright test tests/login/       # Run specific folder

关键词

playwright,端到端,测试,页面对象模型,选择器,端到端,mcp

评论

支持 Markdown