Skip to main content سوق المهارات اكتشف واستكشف مهارات الذكاء الاصطناعي التي بناها المجتمع.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
نسخ Promptعرض تفاصيل Prompt يتجاوز الأمر المباشر Prompt المخصّص للمراجعة. افحص المصدر قبل تشغيله.
npx skills add https://github.com/echoVic/boss-skill --skill qa-e2e-playwrightيبقى الأمر في سطر واحد. مرّر أفقيًا لمراجعته كاملًا قبل النسخ.
تفضّل نسخة محلية؟ نزّل الملفات المتاحة حاليًا لدى SkillsMP.
تحميل Zip جاري التحميل... المزيد من هذا المستودع 可审计的 agent 团队:BMAD 全自动研发流水线编排器。编排 9 个专业 Agent(PM、架构师、UI Designer、Tech Lead、Scrum Master、Frontend、Backend、QA、DevOps)从需求到部署,每一步都有事件溯源 + 不可绕过门禁 + 确定性 eval——可验证测试真跑、门禁真过。支持单环节切片命令(/boss:plan /review /qa /ship)与无 CLI 纯 Markdown 降级。
Triggers: 'boss mode', '/boss', '全自动开发', '从需求到部署', '帮我做一个', 'build this', 'ship it', '全流程', '自动化开发', '一键开发', 'start a project', 'new feature'
Does NOT trigger:
- 单文件修改或简单 bug 修复(直接编辑即可)
- 纯代码阅读或解释(使用 read 工具)
- 已有 pipeline 正在运行时的重复启动
- 极小事(预计 <30 分钟人工可完成、不需要 PRD/架构/门禁记录)
Output: 完整项目代码 + PRD/架构/UI/测试/部署文档,写入 .boss/<feature>/ 目录
ui-designer-design-variants 设计变体模式,产出2-3个设计方案及 tradeoff 分析,供用户选择后确定最终方案
后端测试编写指南,包括单元测试、集成测试和E2E测试的编写方法和最佳实践
المهن ذات الصلة SOC
استنادا إلى تصنيف SOC المهني
name qa/e2e-playwright description Playwright E2E 测试完整方法论,涵盖项目初始化、Page Object Model、认证复用、API Mock、视觉回归、多浏览器测试、CI 集成和调试技巧 version 1.0.0 agent qa type methodology user-invocable false agent-invocable true dependencies ["shared/tech-stack-detection"] triggers ["需要编写或执行 E2E 测试时","需要配置 Playwright 测试环境时","门禁要求 E2E 测试通过时","需要视觉回归测试时"]
Playwright E2E 测试方法论
适用场景
Web 项目需要编写端到端测试
门禁(Gate 1)要求 E2E 测试通过
需要覆盖关键用户流程的自动化验证
需要多浏览器/多视口兼容性验证
需要视觉回归测试
1. 项目初始化
1.1 安装
npm init playwright@latest
npm install -D @playwright/test
npx playwright install
1.2 配置文件(playwright.config.ts)
import { defineConfig, devices } from '@playwright/test' ;
export default defineConfig ({
testDir : './e2e' ,
outputDir : './e2e/test-results' ,
timeout : 30_000 ,
expect : { timeout : 5_000 },
fullyParallel : true ,
workers : process.env .CI ? 1 : undefined ,
retries : process.env .CI ? 1 : 0 ,
reporter : [
['html' , { outputFolder : './e2e/playwright-report' }],
['json' , { outputFile : './e2e/test-results/results.json' }],
...(process.env . ? [[ ] ] : []),
],
: {
: process. . || ,
: ,
: ,
: ,
},
: [
{ : , : { ...devices[ ] } },
{ : , : { ...devices[ ] } },
{ : , : { ...devices[ ] } },
{ : , : { ...devices[ ] } },
{ : , : { ...devices[ ] } },
],
: {
: ,
: ,
: !process. . ,
: ,
},
});
CI
'github'
as
const
use
baseURL
env
BASE_URL
'http://localhost:3000'
screenshot
'only-on-failure'
trace
'on-first-retry'
video
'on-first-retry'
projects
name
'chromium'
use
'Desktop Chrome'
name
'firefox'
use
'Desktop Firefox'
name
'webkit'
use
'Desktop Safari'
name
'mobile-chrome'
use
'Pixel 5'
name
'mobile-safari'
use
'iPhone 13'
webServer
command
'npm run dev'
url
'http://localhost:3000'
reuseExistingServer
env
CI
timeout
120_000
配置项 作用 建议值 fullyParallel测试文件间并行执行 trueworkers并行 worker 数 CI 为 1,本地默认 retries失败重试次数 CI 为 1,本地为 0 trace失败时生成可视化时间线 on-first-retrywebServer自动启动开发服务器 必须配置
1.3 目录结构 e2e/
├── playwright.config.ts # 配置文件(或放在项目根目录)
├── fixtures/ # 自定义 fixtures
│ ├── base.ts # 扩展 base test
│ └── auth.ts # 认证 fixture
├── pages/ # Page Object Models
│ ├── login.page.ts
│ ├── dashboard.page.ts
│ └── components/ # 可复用组件 POM
│ ├── navbar.component.ts
│ └── modal.component.ts
├── specs/ # 测试用例
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── register.spec.ts
│ ├── dashboard/
│ │ └── dashboard.spec.ts
│ └── crud/
│ └── user-management.spec.ts
├── helpers/ # 测试工具
│ ├── seed.ts # 数据种子
│ └── cleanup.ts # 数据清理
├── test-results/ # 测试产物(gitignore)
└── playwright-report/ # HTML 报告(gitignore)
2. Page Object Model(POM)
2.1 核心原则
每个页面一个 POM 类 :封装定位器和操作方法
不暴露 Locator :外部只调用语义化方法
组件级复用 :导航栏、弹窗等提取为独立组件 POM
2.2 基础 POM
import { type Page , type Locator } from '@playwright/test' ;
export class LoginPage {
private readonly emailInput : Locator ;
private readonly passwordInput : Locator ;
private readonly submitButton : Locator ;
private readonly errorMessage : Locator ;
constructor (private readonly page : Page ) {
this .emailInput = page.getByLabel ('邮箱' );
this .passwordInput = page.getByLabel ('密码' );
this .submitButton = page.getByRole ('button' , { name : '登录' });
this .errorMessage = page.getByRole ('alert' );
}
async goto ( ) {
await this .page .goto ('/login' );
}
async login (email : string , password : string ) {
await this .emailInput .fill (email);
await this .passwordInput .fill (password);
await this .submitButton .click ();
}
async getErrorMessage ( ) {
return this .errorMessage .textContent ();
}
}
2.3 组件 POM
import { type Page , type Locator } from '@playwright/test' ;
export class NavbarComponent {
private readonly userMenu : Locator ;
private readonly logoutButton : Locator ;
constructor (private readonly page : Page ) {
this .userMenu = page.getByTestId ('user-menu' );
this .logoutButton = page.getByRole ('menuitem' , { name : '退出登录' });
}
async logout ( ) {
await this .userMenu .click ();
await this .logoutButton .click ();
}
async getUserDisplayName ( ) {
return this .userMenu .textContent ();
}
}
2.4 定位器优先级 优先级 方法 示例 说明 1 getByRolegetByRole('button', { name: '提交' })无障碍语义,最稳定 2 getByLabelgetByLabel('邮箱')表单元素首选 3 getByPlaceholdergetByPlaceholder('请输入邮箱')备选 4 getByTextgetByText('欢迎回来')静态文本 5 getByTestIdgetByTestId('submit-btn')无语义标记时的兜底 6 CSS/XPath page.locator('.btn-primary')尽量避免
3. 认证状态复用
3.1 Global Setup 方式
import { chromium, type FullConfig } from '@playwright/test' ;
async function globalSetup (config : FullConfig ) {
const browser = await chromium.launch ();
const page = await browser.newPage ();
await page.goto ('http://localhost:3000/login' );
await page.getByLabel ('邮箱' ).fill ('admin@example.com' );
await page.getByLabel ('密码' ).fill ('password' );
await page.getByRole ('button' , { name : '登录' }).click ();
await page.waitForURL ('/dashboard' );
await page.context ().storageState ({ path : './e2e/.auth/admin.json' });
await browser.close ();
}
export default globalSetup;
export default defineConfig ({
globalSetup : './e2e/global-setup.ts' ,
projects : [
{ name : 'public' , testMatch : /public\.spec\.ts/ },
{
name : 'authenticated' ,
use : { storageState : './e2e/.auth/admin.json' },
testIgnore : /public\.spec\.ts/ ,
},
],
});
3.2 多角色认证
import { test as base } from '@playwright/test' ;
type AuthFixtures = {
adminPage : Page ;
userPage : Page ;
};
export const test = base.extend <AuthFixtures >({
adminPage : async ({ browser }, use) => {
const context = await browser.newContext ({
storageState : './e2e/.auth/admin.json' ,
});
const page = await context.newPage ();
await use (page);
await context.close ();
},
userPage : async ({ browser }, use) => {
const context = await browser.newContext ({
storageState : './e2e/.auth/user.json' ,
});
const page = await context.newPage ();
await use (page);
await context.close ();
},
});
4. API Mocking
4.1 使用 page.route 拦截请求 test ('显示用户列表(API Mock)' , async ({ page }) => {
await page.route ('/api/users' , async (route) => {
await route.fulfill ({
status : 200 ,
contentType : 'application/json' ,
body : JSON .stringify ([
{ id : 1 , name : '张三' , email : 'zhang@example.com' },
{ id : 2 , name : '李四' , email : 'li@example.com' },
]),
});
});
await page.goto ('/users' );
await expect (page.getByText ('张三' )).toBeVisible ();
await expect (page.getByText ('李四' )).toBeVisible ();
});
4.2 模拟错误响应 test ('API 失败时显示错误提示' , async ({ page }) => {
await page.route ('/api/users' , async (route) => {
await route.fulfill ({ status : 500 , body : 'Internal Server Error' });
});
await page.goto ('/users' );
await expect (page.getByText ('加载失败' )).toBeVisible ();
await expect (page.getByRole ('button' , { name : '重试' })).toBeVisible ();
});
4.3 Mock 与真实请求混合 test ('部分 API Mock' , async ({ page }) => {
await page.route ('**/api/payment/**' , async (route) => {
await route.fulfill ({
status : 200 ,
body : JSON .stringify ({ success : true , transactionId : 'mock-tx-001' }),
});
});
await page.goto ('/checkout' );
});
4.4 何时用 Mock vs 真实 API 场景 建议 核心用户路径 真实 API (关键路径证据规则)第三方服务(支付、邮件) Mock 错误/边界状态 Mock 加载状态、空数据 Mock 数据量大的列表/分页 Mock + 至少一条真实路径
⚠️ Boss 门禁规则 :核心用户路径只由 Mock 证明的,必须标记为未验证 ,不能作为发布证据。
5. 关键用户流程测试(必须覆盖)
5.1 CRUD 完整流程
import { test, expect } from '@playwright/test' ;
import { UserListPage } from '../../pages/user-list.page' ;
import { UserFormPage } from '../../pages/user-form.page' ;
test.describe ('用户管理 CRUD' , () => {
let userList : UserListPage ;
let userForm : UserFormPage ;
test.beforeEach (async ({ page }) => {
userList = new UserListPage (page);
userForm = new UserFormPage (page);
await userList.goto ();
});
test ('创建 → 编辑 → 删除完整流程' , async ({ page }) => {
await userList.clickAddUser ();
await userForm.fillName ('测试用户' );
await userForm.fillEmail ('test@example.com' );
await userForm.submit ();
await expect (page.getByText ('测试用户' )).toBeVisible ();
await userList.editUser ('测试用户' );
await userForm.fillName ('修改后的用户' );
await userForm.submit ();
await expect (page.getByText ('修改后的用户' )).toBeVisible ();
await expect (page.getByText ('测试用户' )).not .toBeVisible ();
await userList.deleteUser ('修改后的用户' );
await userList.confirmDelete ();
await expect (page.getByText ('修改后的用户' )).not .toBeVisible ();
});
test ('列表分页展示' , async ({ page }) => {
await expect (userList.getTable ()).toBeVisible ();
await userList.goToNextPage ();
await expect (page).toHaveURL (/page=2/ );
});
test ('空列表提示' , async ({ page }) => {
await page.route ('/api/users*' , (route ) =>
route.fulfill ({ status : 200 , body : JSON .stringify ({ data : [], total : 0 }) })
);
await page.reload ();
await expect (page.getByText ('暂无数据' )).toBeVisible ();
});
});
5.2 认证流程
import { test, expect } from '@playwright/test' ;
import { LoginPage } from '../../pages/login.page' ;
test.describe ('登录' , () => {
let loginPage : LoginPage ;
test.beforeEach (async ({ page }) => {
loginPage = new LoginPage (page);
await loginPage.goto ();
});
test ('正确凭据成功登录' , async ({ page }) => {
await loginPage.login ('admin@example.com' , 'password' );
await expect (page).toHaveURL ('/dashboard' );
});
test ('错误凭据显示提示' , async ({ page }) => {
await loginPage.login ('admin@example.com' , 'wrong-password' );
await expect (page.getByRole ('alert' )).toContainText ('密码错误' );
await expect (page).toHaveURL ('/login' );
});
test ('未登录重定向到登录页' , async ({ page }) => {
await page.goto ('/dashboard' );
await expect (page).toHaveURL (/\/login/ );
});
});
5.3 表单验证 test.describe ('表单验证' , () => {
test ('必填字段为空时显示错误' , async ({ page }) => {
await page.goto ('/users/new' );
await page.getByRole ('button' , { name : '提交' }).click ();
await expect (page.getByText ('姓名不能为空' )).toBeVisible ();
await expect (page.getByText ('邮箱不能为空' )).toBeVisible ();
});
test ('邮箱格式错误时显示提示' , async ({ page }) => {
await page.goto ('/users/new' );
await page.getByLabel ('邮箱' ).fill ('invalid-email' );
await page.getByLabel ('邮箱' ).blur ();
await expect (page.getByText ('邮箱格式不正确' )).toBeVisible ();
});
});
6. 视觉回归测试
6.1 截图对比 test ('首页视觉回归' , async ({ page }) => {
await page.goto ('/' );
await page.waitForLoadState ('networkidle' );
await expect (page).toHaveScreenshot ('homepage.png' , {
maxDiffPixelRatio : 0.01 ,
});
});
test ('组件视觉回归' , async ({ page }) => {
await page.goto ('/components/button' );
const button = page.getByTestId ('primary-button' );
await expect (button).toHaveScreenshot ('primary-button.png' );
});
6.2 更新基线
npx playwright test --update-snapshots
npx playwright test homepage.spec.ts --update-snapshots
6.3 注意事项
截图基线需提交到 Git
不同 OS 渲染有差异,CI 中使用 Docker 保证一致性
对动态内容(时间、随机数据)需 Mock 后再截图
maxDiffPixelRatio 容忍微小渲染差异(抗锯齿等)
7. 多浏览器和移动端测试
7.1 配置多 project
projects : [
{ name : 'chromium' , use : { ...devices['Desktop Chrome' ] } },
{ name : 'firefox' , use : { ...devices['Desktop Firefox' ] } },
{ name : 'webkit' , use : { ...devices['Desktop Safari' ] } },
{ name : 'mobile-chrome' , use : { ...devices['Pixel 5' ] } },
{ name : 'mobile-safari' , use : { ...devices['iPhone 13' ] } },
{ name : 'tablet' , use : { ...devices['iPad Pro 11' ] } },
]
7.2 按 project 运行
npx playwright test --project=chromium
npx playwright test --project=mobile-chrome --project=mobile-safari
7.3 响应式断言 test ('移动端显示汉堡菜单' , async ({ page, isMobile }) => {
await page.goto ('/' );
if (isMobile) {
await expect (page.getByTestId ('hamburger-menu' )).toBeVisible ();
await expect (page.getByTestId ('desktop-nav' )).not .toBeVisible ();
} else {
await expect (page.getByTestId ('desktop-nav' )).toBeVisible ();
await expect (page.getByTestId ('hamburger-menu' )).not .toBeVisible ();
}
});
8. 测试数据管理
8.1 数据种子(Seed)
import { request } from '@playwright/test' ;
export async function seedTestData (baseURL : string ) {
const api = await request.newContext ({ baseURL });
await api.post ('/api/test/seed' , {
data : {
users : [
{ name : '测试管理员' , email : 'admin@test.com' , role : 'admin' },
{ name : '测试用户' , email : 'user@test.com' , role : 'user' },
],
},
});
await api.dispose ();
}
8.2 数据清理
import { request } from '@playwright/test' ;
export async function cleanupTestData (baseURL : string ) {
const api = await request.newContext ({ baseURL });
await api.post ('/api/test/cleanup' );
await api.dispose ();
}
8.3 在 Global Setup/Teardown 中使用
import { seedTestData } from './helpers/seed' ;
async function globalSetup ( ) {
await seedTestData ('http://localhost:3000' );
}
export default globalSetup;
import { cleanupTestData } from './helpers/cleanup' ;
async function globalTeardown ( ) {
await cleanupTestData ('http://localhost:3000' );
}
export default globalTeardown;
8.4 原则 原则 说明 测试隔离 每个测试独立运行,不依赖其他测试的数据 可重复 多次运行结果一致 快速清理 使用 API 清理而非 UI 操作 避免硬编码 ID 使用动态创建的数据,不依赖数据库自增 ID
9. CI/CD 集成
9.1 GitHub Actions
name: E2E Tests
on:
push:
branches: [main ]
pull_request:
branches: [main ]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build application
run: npm run build
- name: Run E2E tests
run: npx playwright test
- name: Upload test results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: e2e/playwright-report/
retention-days: 30
- name: Upload trace files
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: e2e/test-results/
retention-days: 7
9.2 Docker 一致性 # e2e/Dockerfile
FROM mcr.microsoft.com/playwright:v1.49.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npx", "playwright", "test"]
9.3 CI 关键配置 配置 CI 值 原因 workers1 CI 资源有限,避免争抢 retries1 减少 flaky 误报 reportergithub + htmlGitHub PR 行内注释 + HTML 归档 traceon-first-retry只在重试时录制,节省空间
10. 调试技巧
10.1 UI 模式(开发首选)
10.2 Trace Viewer
npx playwright show-trace e2e/test-results/specs-auth-login-spec-ts/trace.zip
时间线上的每一步操作
DOM 快照
网络请求日志
控制台输出
10.3 Debug 模式
npx playwright test --debug
npx playwright test login.spec.ts --debug
10.4 代码中暂停 test ('调试用' , async ({ page }) => {
await page.goto ('/login' );
await page.pause ();
});
10.5 录制生成测试
npx playwright codegen http://localhost:3000
10.6 常见 Flaky 原因及修复 原因 症状 修复 动画未完成 元素找到但点击无效 等待动画完成或禁用动画 网络请求延迟 元素内容未更新 使用 waitForResponse 竞态条件 间歇性失败 使用 expect().toBeVisible() 自动等待 数据未就绪 列表为空 使用 waitForLoadState('networkidle') 时间依赖 日期/时间相关断言失败 Mock 时间或使用范围断言
11. 与 Boss 流水线门禁集成
11.1 Gate 1 E2E 检查项 Boss 流水线的 Gate 1 门禁要求 E2E 测试满足:
检查项 要求 说明 E2E 测试存在 e2e/ 或 tests/e2e/ 目录非空不能只有单元测试 关键路径覆盖 CRUD + 认证 + 核心业务流程 至少 5 个核心用户操作 全部通过 无失败用例 失败则门禁不通过 真实 API 证据 核心路径不能只有 Mock Mock-only 标记为未验证
11.2 执行命令
npx playwright test --reporter=json --output=e2e/test-results
npx playwright test --grep @critical
npx playwright test
11.3 测试标签 test ('@critical 登录流程' , async ({ page }) => { });
test ('@critical 核心业务流程' , async ({ page }) => { });
test ('@smoke 首页加载' , async ({ page }) => { });
test ('@regression 边界情况' , async ({ page }) => { });
npx playwright test --grep @critical
npx playwright test
12. 检查清单 Agent 编写 E2E 测试时,对照以下清单确认完整性:
输出要求
playwright.config.ts :完整配置
e2e/pages/*.page.ts :Page Object Model
e2e/specs/**/*.spec.ts :测试用例(覆盖关键路径)
认证 fixture :storageState 或 global-setup
CI 配置 :GitHub Actions 或等价 CI 流程
.gitignore 更新 :排除测试产物目录