11 KiB
11 KiB
测试生成
使用场景:从交互式 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 在生产测试中推荐的基于角色的定位器。
工作流程:
- 打开页面 → 生成
await page.goto(url) - 拍摄快照 → 查看元素引用及其可访问角色
- 交互 → 每个操作生成一行 Playwright 代码
- 收集代码 → 将生成的代码行组装成完整的测试
录制操作流程
示例:登录流程
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——保存复杂流程的前/后快照,以便后续编写断言