# 测试生成 > **使用场景**:从交互式 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`**——保存复杂流程的前/后快照,以便后续编写断言