name: playwright-bdd-step-definitions user-invocable: false description: 用于使用 createBdd() 创建步骤定义函数,实现 Page Object Model 模式,并在步骤之间共享 fixtures。 allowed-tools: [Read, Write, Edit, Bash, Glob, Grep]
Playwright BDD 步骤定义
专家知识:创建 Playwright BDD 的步骤定义,包括步骤函数、参数类型、fixtures 和 Page Object Model 集成。
概述
步骤定义将特征文件中的 Gherkin 步骤连接到可执行代码。Playwright BDD 使用 createBdd() 生成类型安全的步骤定义函数,这些函数与 Playwright 的 fixtures 和断言集成。
基本步骤定义
创建步骤函数
// steps/common.steps.ts
import { createBdd } from 'playwright-bdd';
const { Given, When, Then } = createBdd();
Given('我在主页上', async ({ page }) => {
await page.goto('/');
});
When('我点击登录按钮', async ({ page }) => {
await page.getByRole('button', { name: 'Login' }).click();
});
Then('我应该看到仪表板', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
使用 Playwright Fixtures 的步骤
所有 Playwright fixtures 在步骤定义中可用:
import { createBdd } from 'playwright-bdd';
const { Given, When, Then } = createBdd();
Given('我以 {string} 身份登录', async ({ page, context }, username: string) => {
// 访问 page 和 context fixtures
await page.goto('/login');
await page.getByLabel('Username').fill(username);
await page.getByRole('button', { name: 'Login' }).click();
});
When('我截取屏幕截图', async ({ page }) => {
await page.screenshot({ path: 'screenshot.png' });
});
Then('浏览器有 {int} 个 cookies', async ({ context }, count: number) => {
const cookies = await context.cookies();
expect(cookies).toHaveLength(count);
});
参数类型
内置参数
// 字符串参数: {string}
Given('用户名是 {string}', async ({}, name: string) => {
console.log(name); // "John"
});
// 整数参数: {int}
When('我等待 {int} 秒', async ({}, seconds: number) => {
await page.waitForTimeout(seconds * 1000);
});
// 浮点数参数: {float}
Then('价格是 {float}', async ({}, price: number) => {
console.log(price); // 19.99
});
// 单词参数: {word}
Given('我在 {word} 页面上', async ({ page }, pageName: string) => {
await page.goto(`/${pageName}`);
});
匿名参数
使用正则表达式捕获组进行灵活匹配:
// 匹配引号中的任何文本
Given(/^我在搜索框中输入 "(.*)"$/, async ({ page }, query: string) => {
await page.getByRole('searchbox').fill(query);
});
// 匹配数字
When(/^我将 (\d+) 个商品添加到购物车$/, async ({ page }, count: string) => {
const quantity = parseInt(count, 10);
for (let i = 0; i < quantity; i++) {
await page.getByRole('button', { name: 'Add to Cart' }).click();
}
});
自定义参数类型
定义可重用的参数类型:
// steps/parameters.ts
import { defineParameterType } from 'playwright-bdd';
defineParameterType({
name: 'color',
regexp: /red|green|blue/,
transformer: (s) => s,
});
defineParameterType({
name: 'boolean',
regexp: /true|false/,
transformer: (s) => s === 'true',
});
defineParameterType({
name: 'date',
regexp: /\d{4}-\d{2}-\d{2}/,
transformer: (s) => new Date(s),
});
使用自定义参数:
// steps/ui.steps.ts
import { createBdd } from 'playwright-bdd';
import './parameters'; // 导入参数定义
const { Given, When, Then } = createBdd();
When('我选择 {color} 主题', async ({ page }, color: string) => {
await page.getByRole('button', { name: color }).click();
});
Then('深色模式是 {boolean}', async ({ page }, enabled: boolean) => {
if (enabled) {
await expect(page.locator('body')).toHaveClass(/dark/);
}
});
自定义 Fixtures
创建自定义 Fixtures
// steps/fixtures.ts
import { test as base, createBdd } from 'playwright-bdd';
// 定义 fixture 类型
type TestFixtures = {
todoPage: TodoPage;
apiClient: ApiClient;
};
// 扩展基础测试并添加 fixtures
export const test = base.extend<TestFixtures>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await use(todoPage);
},
apiClient: async ({ request }, use) => {
const client = new ApiClient(request);
await use(client);
},
});
// 使用自定义测试创建 BDD 函数
export const { Given, When, Then } = createBdd(test);
在步骤中使用自定义 Fixtures
// steps/todo.steps.ts
import { Given, When, Then } from './fixtures';
Given('我有一个空的任务列表', async ({ todoPage }) => {
await todoPage.goto();
await todoPage.clearAll();
});
When('我添加一个任务 {string}', async ({ todoPage }, text: string) => {
await todoPage.addTodo(text);
});
Then('我应该看到 {int} 个任务', async ({ todoPage }, count: number) => {
await todoPage.expectTodoCount(count);
});
带有设置和清理的 Fixture
// steps/fixtures.ts
import { test as base, createBdd } from 'playwright-bdd';
export const test = base.extend<{
authenticatedPage: Page;
}>({
authenticatedPage: async ({ page, context }, use) => {
// 设置:测试前登录
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
// 使用已认证的页面
await use(page);
// 清理:测试后登出
await page.goto('/logout');
},
});
export const { Given, When, Then } = createBdd(test);
Page Object Model
页面对象定义
// pages/TodoPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class TodoPage {
readonly page: Page;
readonly input: Locator;
readonly list: Locator;
readonly items: Locator;
constructor(page: Page) {
this.page = page;
this.input = page.getByPlaceholder('What needs to be done?');
this.list = page.getByRole('list');
this.items = page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('/todos');
}
async addTodo(text: string) {
await this.input.fill(text);
await this.input.press('Enter');
}
async removeTodo(text: string) {
const item = this.items.filter({ hasText: text });
await item.hover();
await item.getByRole('button', { name: 'Delete' }).click();
}
async toggleTodo(text: string) {
const item = this.items.filter({ hasText: text });
await item.getByRole('checkbox').click();
}
async expectTodoCount(count: number) {
await expect(this.items).toHaveCount(count);
}
async expectTodoVisible(text: string) {
await expect(this.items.filter({ hasText: text })).toBeVisible();
}
async clearAll() {
const count = await this.items.count();
for (let i = count - 1; i >= 0; i--) {
await this.items.nth(i).hover();
await this.items.nth(i).getByRole('button', { name: 'Delete' }).click();
}
}
}
将页面对象与步骤集成
// steps/fixtures.ts
import { test as base, createBdd } from 'playwright-bdd';
import { TodoPage } from '../pages/TodoPage';
import { LoginPage } from '../pages/LoginPage';
export const test = base.extend<{
todoPage: TodoPage;
loginPage: LoginPage;
}>({
todoPage: async ({ page }, use) => {
await use(new TodoPage(page));
},
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
});
export const { Given, When, Then } = createBdd(test);
// steps/todo.steps.ts
import { Given, When, Then } from './fixtures';
Given('我在任务页面上', async ({ todoPage }) => {
await todoPage.goto();
});
When('我将 {string} 添加到我的任务中', async ({ todoPage }, text: string) => {
await todoPage.addTodo(text);
});
When('我完成任务 {string}', async ({ todoPage }, text: string) => {
await todoPage.toggleTodo(text);
});
When('我移除任务 {string}', async ({ todoPage }, text: string) => {
await todoPage.removeTodo(text);
});
Then('我应该看到任务 {string}', async ({ todoPage }, text: string) => {
await todoPage.expectTodoVisible(text);
});
Then('应该有 {int} 个任务', async ({ todoPage }, count: number) => {
await todoPage.expectTodoCount(count);
});
装饰器步骤
在类中使用装饰器
// steps/TodoSteps.ts
import { Fixture, Given, When, Then } from 'playwright-bdd/decorators';
import { test } from './fixtures';
export
@Fixture<typeof test>('todoPage')
class TodoSteps {
@Given('我在任务页面上')
async gotoTodoPage() {
await this.todoPage.goto();
}
@When('我将 {string} 添加到我的任务中')
async addTodo(text: string) {
await this.todoPage.addTodo(text);
}
@Then('我应该看到 {int} 个任务')
async checkTodoCount(count: number) {
await this.todoPage.expectTodoCount(count);
}
}
装饰器中的多个 Fixtures
// steps/AuthenticatedSteps.ts
import { Fixture, Given, When, Then } from 'playwright-bdd/decorators';
import { test } from './fixtures';
export
@Fixture<typeof test>('loginPage')
@Fixture<typeof test>('todoPage')
class AuthenticatedSteps {
@Given('我已登录')
async login() {
await this.loginPage.login('user@example.com', 'password');
}
@When('我访问我的任务')
async visitTodos() {
await this.todoPage.goto();
}
}
数据表格
简单表格
When 我添加以下任务:
| Buy milk |
| Buy bread |
| Buy eggs |
import { DataTable } from '@cucumber/cucumber';
When('我添加以下任务:', async ({ todoPage }, table: DataTable) => {
const todos = table.raw().flat();
for (const todo of todos) {
await todoPage.addTodo(todo);
}
});
带标题的表格
When 我创建用户:
| name | email | role |
| Alice | alice@test.com | admin |
| Bob | bob@test.com | user |
When('我创建用户:', async ({ page }, table: DataTable) => {
const users = table.hashes(); // [{ name: 'Alice', email: '...', role: 'admin' }, ...]
for (const user of users) {
await page.getByLabel('Name').fill(user.name);
await page.getByLabel('Email').fill(user.email);
await page.getByLabel('Role').selectOption(user.role);
await page.getByRole('button', { name: 'Create' }).click();
}
});
垂直表格
When 我填写表单:
| Name | John Doe |
| Email | john@example.com|
| Password | secret123 |
When('我填写表单:', async ({ page }, table: DataTable) => {
const data = table.rowsHash(); // { Name: 'John Doe', Email: '...', Password: '...' }
await page.getByLabel('Name').fill(data.Name);
await page.getByLabel('Email').fill(data.Email);
await page.getByLabel('Password').fill(data.Password);
});
Doc Strings
多行文本输入
When 我写以下评论:
"""
这个产品太棒了!
我强烈推荐给每个人。
五星好评!
"""
When('我写以下评论:', async ({ page }, docString: string) => {
await page.getByRole('textbox', { name: 'Review' }).fill(docString);
});
JSON 数据
When 我发送 API 请求:
"""json
{
"name": "Test Product",
"price": 29.99,
"category": "electronics"
}
"""
When('我发送 API 请求:', async ({ request }, docString: string) => {
const data = JSON.parse(docString);
await request.post('/api/products', { data });
});
在步骤之间共享状态
使用 World/Context
// steps/fixtures.ts
import { test as base, createBdd } from 'playwright-bdd';
type World = {
currentUser?: { id: string; name: string };
createdItems: string[];
};
export const test = base.extend<{ world: World }>({
world: async ({}, use) => {
await use({
createdItems: [],
});
},
});
export const { Given, When, Then } = createBdd(test);
// steps/user.steps.ts
import { Given, When, Then } from './fixtures';
Given('我以 {string} 身份登录', async ({ page, world }, name: string) => {
world.currentUser = { id: '123', name };
await page.goto('/login');
// ... 登录步骤
});
When('我创建项目 {string}', async ({ world }, item: string) => {
world.createdItems.push(item);
// ... 创建项目
});
Then('我应该看到我的项目', async ({ page, world }) => {
for (const item of world.createdItems) {
await expect(page.getByText(item)).toBeVisible();
}
});
错误处理
待定步骤
Given('一个尚未实现的功能', async ({}) => {
throw new Error('步骤未实现');
});
条件性跳过步骤
Given('我在一个支持的浏览器上', async ({ browserName }) => {
if (browserName === 'webkit') {
test.skip(true, 'WebKit 不支持该功能');
}
// 继续测试
});
最佳实践
步骤可重用性
编写适用于多个场景的通用步骤:
// 通用导航步骤
Given('我在 {string} 页面上', async ({ page }, pageName: string) => {
const routes: Record<string, string> = {
home: '/',
login: '/login',
dashboard: '/dashboard',
settings: '/settings',
};
await page.goto(routes[pageName] || `/${pageName}`);
});
// 通用表单填充步骤
When('我在 {string} 中填写 {string}', async ({ page }, label: string, value: string) => {
await page.getByLabel(label).fill(value);
});
// 通用点击步骤
When('我点击 {string}', async ({ page }, text: string) => {
await page.getByRole('button', { name: text }).or(
page.getByRole('link', { name: text })
).click();
});
步骤组织
按领域组织步骤:
steps/
├── fixtures.ts # 共享 fixtures
├── parameters.ts # 自定义参数类型
├── common/
│ ├── navigation.steps.ts
│ └── forms.steps.ts
├── auth/
│ ├── login.steps.ts
│ └── logout.steps.ts
└── products/
├── catalog.steps.ts
└── cart.steps.ts
断言
使用 Playwright 的 expect 进行断言:
import { expect } from '@playwright/test';
Then('我应该看到 {string}', async ({ page }, text: string) => {
await expect(page.getByText(text)).toBeVisible();
});
Then('页面标题应该是 {string}', async ({ page }, title: string) => {
await expect(page).toHaveTitle(title);
});
Then('URL 应该包含 {string}', async ({ page }, path: string) => {
await expect(page).toHaveURL(new RegExp(path));
});
何时使用此技能
- 创建新的步骤定义
- 使用 BDD 实现 Page Object Model
- 设置自定义 fixtures
- 使用数据表格和 doc strings
- 在步骤之间共享状态
- 编写可重用的通用步骤
- 将 Cucumber 步骤转换为 Playwright BDD
- 故障排除步骤匹配问题