playwright

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MCP Workflow (MANDATORY If Available)

MCP流程(若可用则为强制要求)

⚠️ If you have Playwright MCP tools, ALWAYS use them BEFORE creating any test:
  1. Navigate to target page
  2. Take snapshot to see page structure and elements
  3. Interact with forms/elements to verify exact user flow
  4. Take screenshots to document expected states
  5. Verify page transitions through complete flow (loading, success, error)
  6. Document actual selectors from snapshots (use real refs and labels)
  7. Only after exploring create test code with verified selectors
If MCP NOT available: Proceed with test creation based on docs and code analysis.
Why This Matters:
  • ✅ Precise tests - exact steps needed, no assumptions
  • ✅ Accurate selectors - real DOM structure, not imagined
  • ✅ Real flow validation - verify journey actually works
  • ✅ Avoid over-engineering - minimal tests for what exists
  • ✅ Prevent flaky tests - real exploration = stable tests
  • ❌ Never assume how UI "should" work
⚠️ 如果你拥有Playwright MCP工具,在创建任何测试前请务必使用它们:
  1. 导航至目标页面
  2. 拍摄快照以查看页面结构和元素
  3. 交互表单/元素以验证精确的用户流程
  4. 截取屏幕截图以记录预期状态
  5. 验证页面跳转完成完整流程(加载中、成功、错误状态)
  6. 记录快照中的实际选择器(使用真实的引用和标签)
  7. 仅在探索完成后使用已验证的选择器创建测试代码
若MCP不可用: 根据文档和代码分析进行测试创建。
为何这很重要:
  • ✅ 精准测试 - 所需步骤明确,无主观假设
  • ✅ 准确的选择器 - 基于真实DOM结构,而非想象
  • ✅ 真实流程验证 - 确认用户旅程确实可行
  • ✅ 避免过度设计 - 仅针对现有内容编写最精简的测试
  • ✅ 防止不稳定测试 - 真实探索带来稳定的测试
  • ❌ 绝不要假设UI“应该”如何工作

File Structure

文件结构

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
File Naming:
  • sign-up.spec.ts
    (all sign-up tests)
  • sign-up-page.ts
    (page object)
  • sign-up.md
    (documentation)
  • sign-up-critical-path.spec.ts
    (WRONG - no separate files)
  • sign-up-validation.spec.ts
    (WRONG)
tests/
├── base-page.ts              # 所有页面的父类
├── helpers.ts                # 共享工具函数
└── {page-name}/
    ├── {page-name}-page.ts   # 页面对象模型
    ├── {page-name}.spec.ts   # 所有测试都在此文件中(禁止拆分!)
    └── {page-name}.md        # 测试文档
文件命名规则:
  • sign-up.spec.ts
    (所有注册相关测试)
  • sign-up-page.ts
    (页面对象)
  • sign-up.md
    (文档)
  • sign-up-critical-path.spec.ts
    (错误 - 禁止拆分文件)
  • sign-up-validation.spec.ts
    (错误)

Selector Priority (REQUIRED)

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

typescript
// 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
typescript
// 1. 最优选择 - 对交互元素使用getByRole
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navLink = page.getByRole("link", { name: "Dashboard" });

// 2. 最优选择 - 对表单控件使用getByLabel
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");

// 3. 谨慎使用 - 仅对静态内容使用getByText
this.errorMessage = page.getByText("Invalid credentials");
this.pageTitle = page.getByText("Welcome");

// 4. 最后手段 - 当以上方法都失败时使用getByTestId
this.customWidget = page.getByTestId("date-picker");

// ❌ 避免脆弱的选择器
this.button = page.locator(".btn-primary");  // 错误
this.input = page.locator("#email");         // 错误

Scope Detection (ASK IF AMBIGUOUS)

范围检测(若有歧义请确认)

User SaysAction
"a test", "one test", "new test", "add test"Create ONE test() in existing spec
"comprehensive tests", "all tests", "test suite", "generate tests"Create full suite
Examples:
  • "Create a test for user sign-up" → ONE test only
  • "Generate E2E tests for login page" → Full suite
  • "Add a test to verify form validation" → ONE test to existing spec
用户表述操作
"a test", "one test", "new test", "add test"在现有spec文件中创建一个test()
"comprehensive tests", "all tests", "test suite", "generate tests"创建完整测试套件
示例:
  • "Create a test for user sign-up" → 仅创建一个测试
  • "Generate E2E tests for login page" → 创建完整套件
  • "Add a test to verify form validation" → 在现有spec文件中添加一个测试

Page Object Pattern

页面对象模型模式

typescript
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");
  }
}
typescript
import { Page, Locator, expect } from "@playwright/test";

// BasePage - 所有页面都继承此类
export class BasePage {
  constructor(protected page: Page) {}

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

  // 通用方法放在这里(参考重构指南)
  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);
  }
}

// 页面专属实现
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");
  }
}

Page Object Reuse (CRITICAL)

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

Always check existing page objects before creating new ones!
typescript
// ✅ 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
}
Guidelines:
  • Check
    tests/
    for existing page objects first
  • Import and reuse existing pages
  • Create page objects only when page doesn't exist
  • If test requires multiple pages, ensure all page objects exist (create if needed)
创建新页面对象前,请务必检查现有页面对象!
typescript
// ✅ 良好实践:复用现有页面对象
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);  // 复用
  const homePage = new HomePage(page);      // 复用

  await signUpPage.signUp(userData);
  await homePage.verifyPageLoaded();  // 复用方法
  await homePage.signOut();           // 复用方法
  await signInPage.login(credentials); // 复用方法
});

// ❌ 不良实践:重复创建已有功能
export class SignUpPage extends BasePage {
  async logout() { /* ... */ }  // ❌ HomePage已包含此方法
  async login() { /* ... */ }   // ❌ SignInPage已包含此方法
}
指南:
  • 首先检查
    tests/
    目录下的现有页面对象
  • 导入并复用现有页面
  • 仅当页面不存在时才创建新的页面对象
  • 若测试需要多个页面,请确保所有页面对象都已存在(若不存在则创建)

Refactoring Guidelines

重构指南

Move to
BasePage
when:

当满足以下条件时,将代码移至
BasePage

  • ✅ Navigation helpers used by multiple pages (
    waitForPageLoad()
    ,
    getCurrentUrl()
    )
  • ✅ Common UI interactions (notifications, modals, theme toggles)
  • ✅ Verification patterns repeated across pages (
    isVisible()
    ,
    waitForVisible()
    )
  • ✅ Error handling that applies to all pages
  • ✅ Screenshot utilities for debugging
  • ✅ 多个页面共用的导航辅助方法(
    waitForPageLoad()
    ,
    getCurrentUrl()
  • ✅ 通用UI交互(通知、模态框、主题切换)
  • ✅ 跨页面重复的验证模式(
    isVisible()
    ,
    waitForVisible()
  • ✅ 适用于所有页面的错误处理
  • ✅ 用于调试的截图工具

Move to
helpers.ts
when:

当满足以下条件时,将代码移至
helpers.ts

  • ✅ Test data generation (
    generateUniqueEmail()
    ,
    generateTestUser()
    )
  • ✅ Setup/teardown utilities (
    createTestUser()
    ,
    cleanupTestData()
    )
  • ✅ Custom assertions (
    expectNotificationToContain()
    )
  • ✅ API helpers for test setup (
    seedDatabase()
    ,
    resetState()
    )
  • ✅ Time utilities (
    waitForCondition()
    ,
    retryAction()
    )
Before (BAD):
typescript
// 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!
  }
}
After (GOOD):
typescript
// 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!",
  };
}
  • ✅ 测试数据生成(
    generateUniqueEmail()
    ,
    generateTestUser()
  • ✅ 初始化/清理工具(
    createTestUser()
    ,
    cleanupTestData()
  • ✅ 自定义断言(
    expectNotificationToContain()
  • ✅ 用于测试初始化的API辅助方法(
    seedDatabase()
    ,
    resetState()
  • ✅ 时间工具(
    waitForCondition()
    ,
    retryAction()
重构前(不良实践):
typescript
// 在多个页面对象中重复出现
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"]');  // 重复代码!
  }
}
重构后(良好实践):
typescript
// BasePage - 所有页面共享
export class BasePage {
  async waitForNotification(): Promise<void> {
    await this.page.waitForSelector('[role="status"]');
  }
}

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

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

Test Pattern with Tags

带标签的测试模式

typescript
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: "user@test.com", password: "pass123" });

      await expect(page).toHaveURL("/dashboard");
    }
  );
});
Tag Categories:
  • Priority:
    @critical
    ,
    @high
    ,
    @medium
    ,
    @low
  • Type:
    @e2e
  • Feature:
    @signup
    ,
    @signin
    ,
    @dashboard
  • Test ID:
    @SIGNUP-E2E-001
    ,
    @LOGIN-E2E-002
typescript
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: "user@test.com", password: "pass123" });

      await expect(page).toHaveURL("/dashboard");
    }
  );
});
标签分类:
  • 优先级:
    @critical
    ,
    @high
    ,
    @medium
    ,
    @low
  • 类型:
    @e2e
  • 功能:
    @signup
    ,
    @signin
    ,
    @dashboard
  • 测试ID:
    @SIGNUP-E2E-001
    ,
    @LOGIN-E2E-002

Test Documentation Format ({page-name}.md)

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

markdown
undefined
markdown
undefined

E2E Tests: {Feature Name}

E2E Tests: {Feature Name}

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

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

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

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}
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:

Flow Steps:

  1. {Step 1}
  2. {Step 2}
  3. {Step 3}
  1. {Step 1}
  2. {Step 2}
  3. {Step 3}

Expected Result:

Expected Result:

  • {Expected outcome 1}
  • {Expected outcome 2}
  • {Expected outcome 1}
  • {Expected outcome 2}

Key verification points:

Key verification points:

  • {Assertion 1}
  • {Assertion 2}
  • {Assertion 1}
  • {Assertion 2}

Notes:

Notes:

  • {Additional considerations}

**Documentation Rules:**
- ❌ NO general test running instructions
- ❌ NO file structure explanations
- ❌ NO code examples or tutorials
- ❌ NO troubleshooting sections
- ✅ Focus ONLY on specific test case
- ✅ Keep under 60 lines when possible
  • {Additional considerations}

**文档规则:**
- ❌ 禁止包含通用测试运行说明
- ❌ 禁止包含文件结构解释
- ❌ 禁止包含代码示例或教程
- ❌ 禁止包含故障排除章节
- ✅ 仅聚焦于具体测试用例
- ✅ 尽可能保持在60行以内

Commands

命令

bash
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
bash
npx playwright test                    # 运行所有测试
npx playwright test --grep "login"     # 按名称过滤测试
npx playwright test --ui               # 交互式UI模式
npx playwright test --debug            # 调试模式
npx playwright test tests/login/       # 运行指定目录下的测试

Prowler-Specific Patterns

Prowler专属模式

For Prowler UI E2E testing with authentication setup, environment variables, and test IDs, see:
  • Documentation: references/prowler-e2e.md
关于带身份验证设置、环境变量和测试ID的Prowler UI E2E测试,请参考:
  • 文档: references/prowler-e2e.md