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

337 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 测试生成
> **使用场景**:从交互式 CLI 会话生成 Playwright 测试代码——录制用户操作流程、构建测试脚手架、将手动测试转化为自动化测试。
> **前置条件**[core-commands.md](core-commands.md) 基础 CLI 用法
## 快速参考
```bash
# 每个 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. **收集代码** → 将生成的代码行组装成完整的测试
## 录制操作流程
### 示例:登录流程
```bash
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 测试:
```typescript
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()
})
```
```javascript
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。
## 录制复杂操作流程
### 电商结算
```bash
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"
```
### 多步骤向导
```bash
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"
```
### 带动态结果的搜索
```bash
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();
```
## 添加断言
生成的代码捕获的是**操作**而非**断言**。务必手动添加断言以创建有意义的测试。
### 常用断言
```typescript
// 导航后 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")
```
### 断言的放置位置
```typescript
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. 先探索再录制
先拍摄快照了解页面结构,不要盲目点击——先弄清有哪些元素可用:
```bash
playwright-cli open https://example.com
playwright-cli snapshot
# 审查元素,规划操作流程,然后开始交互
```
### 2. 使用语义化定位器
生成的代码已优先使用基于角色的定位器。如果在生成输出中看到 CSS 选择器,请考虑提交 issue——基于角色的定位器更具弹性:
```typescript
// 生成(好——语义化,有弹性)
await page.getByRole("button", { name: "Submit" }).click()
// 避免手动编写(脆弱——CSS 变化即失效)
await page.locator("#submit-btn").click()
await page.locator(".btn.btn-primary").click()
```
### 3. 保持测试专注
一个测试 = 一个用户行为。不要将整个会话录制进一个测试:
```typescript
// 好:专注的测试
test("user can add item to cart", async ({ page }) => {
// 仅限加入购物车流程
})
test("user can complete checkout", async ({ page }) => {
// 仅限结算流程(使用认证状态跳过登录)
})
// 差:单体测试
test("user journey", async ({ page }) => {
// 登录 + 浏览 + 加入购物车 + 结算 + 验证邮件……
})
```
### 4. 参数化测试数据
将录制中的硬编码值替换为变量或测试数据:
```typescript
// 而非录制中的硬编码值
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. 为不稳定步骤添加等待策略
如果录制的操作依赖于异步内容加载,请添加显式等待:
```typescript
// 在点击动态加载的元素之前
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`**——保存复杂流程的前/后快照,以便后续编写断言