Files
2026-07-13 21:36:47 +08:00

11 KiB
Raw Permalink Blame History

测试生成

使用场景:从交互式 CLI 会话生成 Playwright 测试代码——录制用户操作流程、构建测试脚手架、将手动测试转化为自动化测试。 前置条件core-commands.md 基础 CLI 用法

快速参考

# 每个 CLI 操作都会输出等价的 Playwright 代码
playwright-cli open https://example.com/login
playwright-cli snapshot
playwright-cli fill e1 "user@example.com"
# 输出: await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');

playwright-cli fill e2 "password123"
# 输出: await page.getByRole('textbox', { name: 'Password' }).fill('password123');

playwright-cli click e3
# 输出: await page.getByRole('button', { name: 'Sign In' }).click();

工作原理

使用 playwright-cli 执行的每个操作都会自动在输出中生成对应的 Playwright TypeScript 代码。这些代码使用了 Playwright 在生产测试中推荐的基于角色的定位器

工作流程:

  1. 打开页面 → 生成 await page.goto(url)
  2. 拍摄快照 → 查看元素引用及其可访问角色
  3. 交互 → 每个操作生成一行 Playwright 代码
  4. 收集代码 → 将生成的代码行组装成完整的测试

录制操作流程

示例:登录流程

playwright-cli open https://example.com/login
# 已执行 Playwright 代码:
# await page.goto('https://example.com/login');

playwright-cli snapshot
# 输出:
# e1 [textbox "Email"]
# e2 [textbox "Password"]
# e3 [button "Sign In"]
# e4 [link "Forgot password?"]

playwright-cli fill e1 "user@example.com"
# 已执行 Playwright 代码:
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');

playwright-cli fill e2 "password123"
# 已执行 Playwright 代码:
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');

playwright-cli click e3
# 已执行 Playwright 代码:
# await page.getByRole('button', { name: 'Sign In' }).click();

组装成测试用例

收集生成的代码并包装成 Playwright 测试:

import { test, expect } from "@playwright/test"

test("user can log in", async ({ page }) => {
  await page.goto("https://example.com/login")
  await page.getByRole("textbox", { name: "Email" }).fill("user@example.com")
  await page.getByRole("textbox", { name: "Password" }).fill("password123")
  await page.getByRole("button", { name: "Sign In" }).click()

  // 添加断言(非自动生成——需手动添加)
  await expect(page).toHaveURL(/.*dashboard/)
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})
const { test, expect } = require("@playwright/test")

test("user can log in", async ({ page }) => {
  await page.goto("https://example.com/login")
  await page.getByRole("textbox", { name: "Email" }).fill("user@example.com")
  await page.getByRole("textbox", { name: "Password" }).fill("password123")
  await page.getByRole("button", { name: "Sign In" }).click()

  await expect(page).toHaveURL(/.*dashboard/)
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})

生成代码中的定位器策略

生成的代码使用 Playwright 推荐的定位器优先级:

优先级 定位器类型 示例 使用场景
1 基于角色 getByRole('button', { name: 'Submit' }) 具有 ARIA 角色的元素
2 基于标签 getByLabel('Email') 带标签的表单输入
3 基于占位符 getByPlaceholder('Search...') 带占位符文本的输入
4 基于文本 getByText('Welcome back') 静态文本内容
5 测试 ID getByTestId('submit-btn') 带有 data-testid 的元素

这些定位器对标记结构变化具有弹性——它们反映用户感知页面的方式,而不是依赖 CSS 选择器或 XPath。

录制复杂操作流程

电商结算

playwright-cli open https://shop.example.com

# 浏览商品
playwright-cli snapshot
playwright-cli click e5                # "Add to Cart" 按钮
# await page.getByRole('button', { name: 'Add to Cart' }).click();

playwright-cli click e12               # 购物车图标
# await page.getByRole('link', { name: 'Cart' }).click();

playwright-cli snapshot
playwright-cli click e3                # "Proceed to Checkout"
# await page.getByRole('button', { name: 'Proceed to Checkout' }).click();

# 配送信息
playwright-cli snapshot
playwright-cli fill e1 "Jane Doe"
# await page.getByRole('textbox', { name: 'Full Name' }).fill('Jane Doe');
playwright-cli fill e2 "123 Main St"
# await page.getByRole('textbox', { name: 'Address' }).fill('123 Main St');
playwright-cli fill e3 "Springfield"
# await page.getByRole('textbox', { name: 'City' }).fill('Springfield');
playwright-cli select e4 "IL"
# await page.getByRole('combobox', { name: 'State' }).selectOption('IL');
playwright-cli fill e5 "62701"
# await page.getByRole('textbox', { name: 'ZIP Code' }).fill('62701');
playwright-cli click e6               # "Continue to Payment"

多步骤向导

playwright-cli open https://example.com/onboarding

# 步骤 1
playwright-cli snapshot
playwright-cli fill e1 "Acme Corp"
playwright-cli select e2 "technology"
playwright-cli click e3    # 下一步

# 步骤 2
playwright-cli snapshot
playwright-cli check e1    # 功能复选框
playwright-cli check e3    # 另一功能
playwright-cli click e5    # 下一步

# 步骤 3
playwright-cli snapshot
playwright-cli click e2    # "Complete Setup"

带动态结果的搜索

playwright-cli open https://example.com
playwright-cli snapshot

playwright-cli fill e1 "playwright testing"
# await page.getByRole('searchbox', { name: 'Search' }).fill('playwright testing');

playwright-cli press Enter
# await page.keyboard.press('Enter');

playwright-cli snapshot    # 查看搜索结果——分配了新引用

playwright-cli click e3    # 第一个结果
# await page.getByRole('link', { name: 'Getting Started with Playwright' }).click();

添加断言

生成的代码捕获的是操作而非断言。务必手动添加断言以创建有意义的测试。

常用断言

// 导航后 URL 改变
await expect(page).toHaveURL(/.*dashboard/)
await expect(page).toHaveURL("https://example.com/success")

// 元素可见
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible()
await expect(page.getByText("Order confirmed")).toBeVisible()

// 元素包含文本
await expect(page.getByTestId("total")).toHaveText("$99.99")
await expect(page.getByRole("alert")).toContainText("saved")

// 元素具有特定属性
await expect(page.getByRole("button", { name: "Submit" })).toBeDisabled()
await expect(page.getByRole("checkbox")).toBeChecked()

// 元素数量
await expect(page.getByRole("listitem")).toHaveCount(5)

// 页面标题
await expect(page).toHaveTitle(/Dashboard/)

// 截图对比
await expect(page).toHaveScreenshot("checkout.png")

断言的放置位置

test("complete checkout flow", async ({ page }) => {
  await page.goto("https://shop.example.com/products")

  // 操作:将商品加入购物车
  await page.getByRole("button", { name: "Add to Cart" }).click()

  // 断言:购物车角标更新
  await expect(page.getByTestId("cart-count")).toHaveText("1")

  // 操作:前往购物车
  await page.getByRole("link", { name: "Cart" }).click()

  // 断言:页面正确
  await expect(page).toHaveURL(/.*cart/)
  await expect(page.getByRole("heading", { name: "Your Cart" })).toBeVisible()

  // 操作:前往结算
  await page.getByRole("button", { name: "Checkout" }).click()

  // 操作:填写配送信息
  await page.getByRole("textbox", { name: "Full Name" }).fill("Jane Doe")
  await page.getByRole("textbox", { name: "Address" }).fill("123 Main St")
  await page.getByRole("button", { name: "Place Order" }).click()

  // 断言:订单已确认
  await expect(page.getByText("Order confirmed")).toBeVisible()
  await expect(page.getByTestId("order-number")).toBeVisible()
})

最佳实践

1. 先探索再录制

先拍摄快照了解页面结构,不要盲目点击——先弄清有哪些元素可用:

playwright-cli open https://example.com
playwright-cli snapshot
# 审查元素,规划操作流程,然后开始交互

2. 使用语义化定位器

生成的代码已优先使用基于角色的定位器。如果在生成输出中看到 CSS 选择器,请考虑提交 issue——基于角色的定位器更具弹性:

// 生成(好——语义化,有弹性)
await page.getByRole("button", { name: "Submit" }).click()

// 避免手动编写(脆弱——CSS 变化即失效)
await page.locator("#submit-btn").click()
await page.locator(".btn.btn-primary").click()

3. 保持测试专注

一个测试 = 一个用户行为。不要将整个会话录制进一个测试:

// 好:专注的测试
test("user can add item to cart", async ({ page }) => {
  // 仅限加入购物车流程
})

test("user can complete checkout", async ({ page }) => {
  // 仅限结算流程(使用认证状态跳过登录)
})

// 差:单体测试
test("user journey", async ({ page }) => {
  // 登录 + 浏览 + 加入购物车 + 结算 + 验证邮件……
})

4. 参数化测试数据

将录制中的硬编码值替换为变量或测试数据:

// 而非录制中的硬编码值
test("registration", async ({ page }) => {
  const user = {
    name: "Jane Doe",
    email: `test+${Date.now()}@example.com`,
    password: "SecurePass123!",
  }

  await page.goto("/register")
  await page.getByRole("textbox", { name: "Name" }).fill(user.name)
  await page.getByRole("textbox", { name: "Email" }).fill(user.email)
  await page.getByRole("textbox", { name: "Password" }).fill(user.password)
  await page.getByRole("button", { name: "Create Account" }).click()

  await expect(page).toHaveURL(/.*welcome/)
})

5. 为不稳定步骤添加等待策略

如果录制的操作依赖于异步内容加载,请添加显式等待:

// 在点击动态加载的元素之前
await page.waitForSelector(".results-loaded")
await page.getByRole("link", { name: "First Result" }).click()

// 或使用 Playwright 的自动等待(推荐)
await expect(page.getByRole("link", { name: "First Result" })).toBeVisible()
await page.getByRole("link", { name: "First Result" }).click()

提示

  • 生成的代码只是一个起点——在提交到测试套件之前,务必审查、添加断言并参数化
  • 动态变化后重新快照——DOM 更新后引用会改变
  • 结合 state-save——录制一次登录流程,保存状态,然后所有其他录制都从已认证状态开始
  • 为快照使用 --filename——保存复杂流程的前/后快照,以便后续编写断言