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
用户表述操作
"一个测试"、"新增一个测试"、"添加测试"在现有spec文件中创建一个test()
"全面测试"、"所有测试"、"测试套件"、"生成测试"创建完整的测试套件
示例:
  • "创建用户注册的测试" → 仅创建一个测试
  • "为登录页面生成E2E测试" → 创建完整套件
  • "添加一个验证表单校验的测试" → 在现有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.<span class="mathjax">${Date.now()}</span>@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测试:{功能名称}

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

套件ID:
{SUITE-ID}
功能: {功能描述}

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

测试用例:
{TEST-ID}
- {测试用例标题}

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}
优先级:
{critical|high|medium|low}
标签:
  • type → @e2e
  • feature → @{feature-name}
描述/目标: {简要描述}
前置条件:
  • {测试运行的前提条件}
  • {所需数据或状态}

Flow Steps:

流程步骤:

  1. {Step 1}
  2. {Step 2}
  3. {Step 3}
  1. {步骤1}
  2. {步骤2}
  3. {步骤3}

Expected Result:

预期结果:

  • {Expected outcome 1}
  • {Expected outcome 2}
  • {预期结果}
  • {预期结果2}

Key verification points:

关键验证点:

  • {Assertion 1}
  • {Assertion 2}
  • {断言1}
  • {断言2}

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
  • {额外注意事项}

**文档规则:**
- ❌ 禁止包含通用测试运行说明
- ❌ 禁止包含文件结构解释
- ❌ 禁止包含代码示例或教程
- ❌ 禁止包含故障排查章节
- ✅ 仅聚焦于具体测试用例
- ✅ 尽可能控制在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/       # 运行指定目录的测试

Keywords

关键词

playwright, e2e, testing, page object model, selectors, end-to-end, mcp
playwright, e2e, testing, page object model, selectors, end-to-end, mcp",