mcpField
P
playwright

playwright

github.com/prowler-cloud测试与质量15.1k119 次安装1 天前更新
代码仓库

关于 playwright

面向编写 UI 端到端测试的 Playwright 技能。它强调在编码前先用可用的 Playwright MCP 工具探索页面,并通过可复用的页面对象组织测试。

主要功能

  1. 01编写测试前探索页面
  2. 02优先使用角色和标签选择器
  3. 03复用页面对象
  4. 04组织测试与文档
  5. 05按优先级和功能标记测试

适用场景

  1. 01为注册流程编写端到端测试
  2. 02为现有页面规格添加覆盖
  3. 03记录具体测试用例

MCP 相关

若有 Playwright MCP 工具,此技能要求先导航、查看页面快照、交互并验证页面转换,再编写测试。

安装

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

$npx skills add https://github.com/prowler-cloud/prowler --skill playwright

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

附带文件references/prowler-e2e.md

详细介绍

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()
“全面测试”“所有测试”“测试套件”“生成测试”创建完整的测试套件

示例:

  • “为用户注册创建一个测试” → 只创建一个测试
  • “为登录页面生成 E2E 测试” → 创建完整的测试套件
  • “添加一个测试来验证表单校验” → 在现有 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

Prowler 专用模式

有关 Prowler UI E2E 测试中的身份验证设置、环境变量和测试 ID,请参阅:

评论

支持 Markdown