PlaywrightBDD步骤定义技能Skill playwright-bdd-step-definitions

此技能专注于利用Playwright框架实施行为驱动开发(BDD),创建类型安全的步骤定义函数,集成Page Object Model模式和自定义fixtures,用于前端Web应用自动化测试。关键词:Playwright, BDD, 步骤定义, 测试自动化, 前端测试, Page Object Model, 参数类型, 数据表格。

测试 0 次安装 0 次浏览 更新于 3/25/2026

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
  • 故障排除步骤匹配问题