playwright
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMCP Workflow (MANDATORY If Available)
MCP流程(若可用则为强制要求)
⚠️ If you have Playwright MCP tools, ALWAYS use them BEFORE creating any test:
- Navigate to target page
- Take snapshot to see page structure and elements
- Interact with forms/elements to verify exact user flow
- Take screenshots to document expected states
- Verify page transitions through complete flow (loading, success, error)
- Document actual selectors from snapshots (use real refs and labels)
- 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工具,在创建任何测试前请务必使用它们:
- 导航至目标页面
- 拍摄快照以查看页面结构和元素
- 交互表单/元素以验证精确的用户流程
- 截取屏幕截图以记录预期状态
- 验证页面跳转完成完整流程(加载中、成功、错误状态)
- 记录快照中的实际选择器(使用真实的引用和标签)
- 仅在探索完成后使用已验证的选择器创建测试代码
若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 documentationFile Naming:
- ✅ (all sign-up tests)
sign-up.spec.ts - ✅ (page object)
sign-up-page.ts - ✅ (documentation)
sign-up.md - ❌ (WRONG - no separate files)
sign-up-critical-path.spec.ts - ❌ (WRONG)
sign-up-validation.spec.ts
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"); // NOtypescript
// 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 Says | Action |
|---|---|
| "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 for existing page objects first
tests/ - 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当满足以下条件时,将代码移至BasePage
:
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当满足以下条件时,将代码移至helpers.ts
:
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
undefinedmarkdown
undefinedE2E Tests: {Feature Name}
E2E Tests: {Feature Name}
Suite ID:
Feature: {Feature description}
{SUITE-ID}Suite ID:
Feature: {Feature description}
{SUITE-ID}Test Case: {TEST-ID}
- {Test case title}
{TEST-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}
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:
- {Step 1}
- {Step 2}
- {Step 3}
- {Step 1}
- {Step 2}
- {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 folderbash
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