39 KiB
表单与校验
使用场景:测试表单填写、提交、校验信息、多步骤向导、动态字段和自动补全交互。 前置知识:core/locators.md、core/assertions-and-waiting.md
快速参考
// 文本输入
await page.getByLabel("Name").fill("Jane Doe")
// 下拉选择
await page.getByLabel("Country").selectOption("US")
await page.getByLabel("Country").selectOption({ label: "United States" })
// 复选框和单选按钮
await page.getByLabel("Remember me").check()
await page.getByLabel("Express shipping").click()
// 日期输入
await page.getByLabel("Start date").fill("2025-03-15")
// 清空字段
await page.getByLabel("Name").clear()
// 提交
await page.getByRole("button", { name: "Submit" }).click()
// 验证校验错误
await expect(page.getByText("Email is required")).toBeVisible()
模式
填写基本表单字段
使用场景:测试任何包含标准 HTML 输入(文本、邮箱、密码、数字、文本域、下拉选择、复选框、单选按钮)的表单。 避免场景:无。这是基础模式。
TypeScript
import { test, expect } from "@playwright/test"
test("fill and submit a registration form", async ({ page }) => {
await page.goto("/register")
// 文本输入——使用 fill()(会先清空),而非 type()
await page.getByLabel("First name").fill("Jane")
await page.getByLabel("Last name").fill("Doe")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Password", { exact: true }).fill("S3cureP@ss!")
await page.getByLabel("Confirm password").fill("S3cureP@ss!")
// 文本域
await page.getByLabel("Bio").fill("Software engineer with 10 years of experience.")
// 数字输入
await page.getByLabel("Age").fill("32")
// 原生 <select>
await page.getByLabel("Country").selectOption("US")
// 按可见标签文本选择(当值与显示文本不同时)
await page.getByLabel("State").selectOption({ label: "California" })
// 多选
await page.getByLabel("Interests").selectOption(["coding", "testing", "devops"])
// 复选框——使用 check() 而非 click()(幂等:如果已勾选则不会取消勾选)
await page.getByLabel("I agree to the terms").check()
await expect(page.getByLabel("I agree to the terms")).toBeChecked()
// 单选按钮
await page.getByLabel("Monthly billing").check()
await expect(page.getByLabel("Monthly billing")).toBeChecked()
// 提交
await page.getByRole("button", { name: "Create account" }).click()
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible()
})
JavaScript
const { test, expect } = require("@playwright/test")
test("fill and submit a registration form", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("First name").fill("Jane")
await page.getByLabel("Last name").fill("Doe")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Password", { exact: true }).fill("S3cureP@ss!")
await page.getByLabel("Confirm password").fill("S3cureP@ss!")
await page.getByLabel("Bio").fill("Software engineer with 10 years of experience.")
await page.getByLabel("Age").fill("32")
await page.getByLabel("Country").selectOption("US")
await page.getByLabel("State").selectOption({ label: "California" })
await page.getByLabel("Interests").selectOption(["coding", "testing", "devops"])
await page.getByLabel("I agree to the terms").check()
await expect(page.getByLabel("I agree to the terms")).toBeChecked()
await page.getByLabel("Monthly billing").check()
await expect(page.getByLabel("Monthly billing")).toBeChecked()
await page.getByRole("button", { name: "Create account" }).click()
await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible()
})
日期和时间输入
使用场景:测试原生 <input type="date">、<input type="time">、<input type="datetime-local"> 或第三方日期选择器。
避免场景:日期选择器是普通文本字段,没有特殊输入类型。直接使用 fill() 即可。
TypeScript
import { test, expect } from "@playwright/test"
test("fill native date and time inputs", async ({ page }) => {
await page.goto("/booking")
// 原生日期输入——使用 ISO 格式 YYYY-MM-DD
await page.getByLabel("Check-in date").fill("2025-06-15")
await expect(page.getByLabel("Check-in date")).toHaveValue("2025-06-15")
// 原生时间输入——使用 HH:MM 格式
await page.getByLabel("Arrival time").fill("14:30")
// datetime-local——使用 YYYY-MM-DDTHH:MM 格式
await page.getByLabel("Event start").fill("2025-06-15T09:00")
})
test("interact with a third-party date picker", async ({ page }) => {
await page.goto("/booking")
// 点击打开日期选择器
await page.getByLabel("Departure date").click()
// 如有需要,切换月份
await page.getByRole("button", { name: "Next month" }).click()
// 选择特定日期
await page.getByRole("gridcell", { name: "20" }).click()
// 验证选中日期已出现在输入框中
await expect(page.getByLabel("Departure date")).toHaveValue(/2025/)
})
JavaScript
const { test, expect } = require("@playwright/test")
test("fill native date and time inputs", async ({ page }) => {
await page.goto("/booking")
await page.getByLabel("Check-in date").fill("2025-06-15")
await expect(page.getByLabel("Check-in date")).toHaveValue("2025-06-15")
await page.getByLabel("Arrival time").fill("14:30")
await page.getByLabel("Event start").fill("2025-06-15T09:00")
})
test("interact with a third-party date picker", async ({ page }) => {
await page.goto("/booking")
await page.getByLabel("Departure date").click()
await page.getByRole("button", { name: "Next month" }).click()
await page.getByRole("gridcell", { name: "20" }).click()
await expect(page.getByLabel("Departure date")).toHaveValue(/2025/)
})
必填字段校验
使用场景:测试当必填字段为空时,表单是否显示适当的错误信息。 避免场景:你只关心正常流程。校验测试应补充而非取代成功路径测试。
TypeScript
import { test, expect } from "@playwright/test"
test("shows validation errors for empty required fields", async ({ page }) => {
await page.goto("/contact")
// 不填写任何内容直接提交
await page.getByRole("button", { name: "Send message" }).click()
// 验证所有必填字段错误均已出现
await expect(page.getByText("Name is required")).toBeVisible()
await expect(page.getByText("Email is required")).toBeVisible()
await expect(page.getByText("Message is required")).toBeVisible()
// 验证表单未提交(仍停留在同一页面)
await expect(page).toHaveURL(/\/contact/)
})
test("clears validation errors when fields are filled", async ({ page }) => {
await page.goto("/contact")
// 触发错误
await page.getByRole("button", { name: "Send message" }).click()
await expect(page.getByText("Name is required")).toBeVisible()
// 填写字段——错误应消失
await page.getByLabel("Name").fill("Jane Doe")
// 用 Tab 或点击别处触发失焦校验
await page.getByLabel("Email").focus()
await expect(page.getByText("Name is required")).not.toBeVisible()
})
test("native HTML5 validation with required attribute", async ({ page }) => {
await page.goto("/simple-form")
await page.getByRole("button", { name: "Submit" }).click()
// 通过 :invalid 伪类检查原生校验信息
const emailInput = page.getByLabel("Email")
const validationMessage = await emailInput.evaluate(
(el: HTMLInputElement) => el.validationMessage
)
expect(validationMessage).toBeTruthy()
})
JavaScript
const { test, expect } = require("@playwright/test")
test("shows validation errors for empty required fields", async ({ page }) => {
await page.goto("/contact")
await page.getByRole("button", { name: "Send message" }).click()
await expect(page.getByText("Name is required")).toBeVisible()
await expect(page.getByText("Email is required")).toBeVisible()
await expect(page.getByText("Message is required")).toBeVisible()
await expect(page).toHaveURL(/\/contact/)
})
test("clears validation errors when fields are filled", async ({ page }) => {
await page.goto("/contact")
await page.getByRole("button", { name: "Send message" }).click()
await expect(page.getByText("Name is required")).toBeVisible()
await page.getByLabel("Name").fill("Jane Doe")
await page.getByLabel("Email").focus()
await expect(page.getByText("Name is required")).not.toBeVisible()
})
test("native HTML5 validation with required attribute", async ({ page }) => {
await page.goto("/simple-form")
await page.getByRole("button", { name: "Submit" }).click()
const emailInput = page.getByLabel("Email")
const validationMessage = await emailInput.evaluate((el) => el.validationMessage)
expect(validationMessage).toBeTruthy()
})
格式校验与自定义规则
使用场景:测试邮箱格式、电话号码格式、密码强度以及业务特定的校验规则。 避免场景:校验完全是服务端进行的,没有客户端反馈。应通过 API 测试。
TypeScript
import { test, expect } from "@playwright/test"
test("validates email format", async ({ page }) => {
await page.goto("/register")
const emailField = page.getByLabel("Email")
// 无效格式
const invalidEmails = ["not-an-email", "missing@", "@no-local.com", "spaces in@email.com"]
for (const email of invalidEmails) {
await emailField.fill(email)
await emailField.blur()
await expect(page.getByText("Please enter a valid email")).toBeVisible()
}
// 有效格式清除错误
await emailField.fill("valid@example.com")
await emailField.blur()
await expect(page.getByText("Please enter a valid email")).not.toBeVisible()
})
test("validates password strength rules", async ({ page }) => {
await page.goto("/register")
const passwordField = page.getByLabel("Password", { exact: true })
// 太短
await passwordField.fill("Ab1!")
await passwordField.blur()
await expect(page.getByText("At least 8 characters")).toBeVisible()
// 缺少大写字母
await passwordField.fill("abcdefg1!")
await passwordField.blur()
await expect(page.getByText("At least one uppercase letter")).toBeVisible()
// 强密码——所有检查通过
await passwordField.fill("Str0ngP@ss!")
await passwordField.blur()
await expect(page.getByText(/At least/)).not.toBeVisible()
})
test("validates custom business rule — age range", async ({ page }) => {
await page.goto("/insurance/quote")
await page.getByLabel("Age").fill("15")
await page.getByLabel("Age").blur()
await expect(page.getByText("Must be 18 or older")).toBeVisible()
await page.getByLabel("Age").fill("150")
await page.getByLabel("Age").blur()
await expect(page.getByText("Please enter a valid age")).toBeVisible()
await page.getByLabel("Age").fill("30")
await page.getByLabel("Age").blur()
await expect(page.getByText(/Must be|valid age/)).not.toBeVisible()
})
JavaScript
const { test, expect } = require("@playwright/test")
test("validates email format", async ({ page }) => {
await page.goto("/register")
const emailField = page.getByLabel("Email")
const invalidEmails = ["not-an-email", "missing@", "@no-local.com", "spaces in@email.com"]
for (const email of invalidEmails) {
await emailField.fill(email)
await emailField.blur()
await expect(page.getByText("Please enter a valid email")).toBeVisible()
}
await emailField.fill("valid@example.com")
await emailField.blur()
await expect(page.getByText("Please enter a valid email")).not.toBeVisible()
})
test("validates password strength rules", async ({ page }) => {
await page.goto("/register")
const passwordField = page.getByLabel("Password", { exact: true })
await passwordField.fill("Ab1!")
await passwordField.blur()
await expect(page.getByText("At least 8 characters")).toBeVisible()
await passwordField.fill("abcdefg1!")
await passwordField.blur()
await expect(page.getByText("At least one uppercase letter")).toBeVisible()
await passwordField.fill("Str0ngP@ss!")
await passwordField.blur()
await expect(page.getByText(/At least/)).not.toBeVisible()
})
test("validates custom business rule — age range", async ({ page }) => {
await page.goto("/insurance/quote")
await page.getByLabel("Age").fill("15")
await page.getByLabel("Age").blur()
await expect(page.getByText("Must be 18 or older")).toBeVisible()
await page.getByLabel("Age").fill("150")
await page.getByLabel("Age").blur()
await expect(page.getByText("Please enter a valid age")).toBeVisible()
await page.getByLabel("Age").fill("30")
await page.getByLabel("Age").blur()
await expect(page.getByText(/Must be|valid age/)).not.toBeVisible()
})
多步骤表单与向导
使用场景:表单跨多个页面或步骤,包含上一步/下一步导航和每步校验。 避免场景:表单是单页的。使用基本表单填写模式。
TypeScript
import { test, expect } from "@playwright/test"
test("complete a multi-step checkout wizard", async ({ page }) => {
await page.goto("/checkout")
// 步骤 1:收货信息
await test.step("fill shipping information", async () => {
await expect(page.getByRole("heading", { name: "Shipping" })).toBeVisible()
await page.getByLabel("Address").fill("123 Main St")
await page.getByLabel("City").fill("Portland")
await page.getByLabel("State").selectOption("OR")
await page.getByLabel("ZIP code").fill("97201")
await page.getByRole("button", { name: "Continue" }).click()
})
// 步骤 2:支付
await test.step("fill payment details", async () => {
await expect(page.getByRole("heading", { name: "Payment" })).toBeVisible()
await page.getByLabel("Card number").fill("4242424242424242")
await page.getByLabel("Expiration").fill("12/28")
await page.getByLabel("CVC").fill("123")
await page.getByRole("button", { name: "Continue" }).click()
})
// 步骤 3:确认
await test.step("review and confirm order", async () => {
await expect(page.getByRole("heading", { name: "Review" })).toBeVisible()
// 验证前面步骤的数据已显示
await expect(page.getByText("123 Main St")).toBeVisible()
await expect(page.getByText("ending in 4242")).toBeVisible()
await page.getByRole("button", { name: "Place order" }).click()
})
// 确认
await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible()
})
test("wizard validates each step before proceeding", async ({ page }) => {
await page.goto("/checkout")
// 尝试不填必填字段跳过步骤 1
await page.getByRole("button", { name: "Continue" }).click()
// 应停留在步骤 1 并显示校验错误
await expect(page.getByRole("heading", { name: "Shipping" })).toBeVisible()
await expect(page.getByText("Address is required")).toBeVisible()
})
test("wizard supports going back without losing data", async ({ page }) => {
await page.goto("/checkout")
// 填写步骤 1
await page.getByLabel("Address").fill("123 Main St")
await page.getByLabel("City").fill("Portland")
await page.getByLabel("State").selectOption("OR")
await page.getByLabel("ZIP code").fill("97201")
await page.getByRole("button", { name: "Continue" }).click()
// 从步骤 2 返回
await page.getByRole("button", { name: "Back" }).click()
// 验证步骤 1 的数据得以保留
await expect(page.getByLabel("Address")).toHaveValue("123 Main St")
await expect(page.getByLabel("City")).toHaveValue("Portland")
})
JavaScript
const { test, expect } = require("@playwright/test")
test("complete a multi-step checkout wizard", async ({ page }) => {
await page.goto("/checkout")
await test.step("fill shipping information", async () => {
await expect(page.getByRole("heading", { name: "Shipping" })).toBeVisible()
await page.getByLabel("Address").fill("123 Main St")
await page.getByLabel("City").fill("Portland")
await page.getByLabel("State").selectOption("OR")
await page.getByLabel("ZIP code").fill("97201")
await page.getByRole("button", { name: "Continue" }).click()
})
await test.step("fill payment details", async () => {
await expect(page.getByRole("heading", { name: "Payment" })).toBeVisible()
await page.getByLabel("Card number").fill("4242424242424242")
await page.getByLabel("Expiration").fill("12/28")
await page.getByLabel("CVC").fill("123")
await page.getByRole("button", { name: "Continue" }).click()
})
await test.step("review and confirm order", async () => {
await expect(page.getByRole("heading", { name: "Review" })).toBeVisible()
await expect(page.getByText("123 Main St")).toBeVisible()
await expect(page.getByText("ending in 4242")).toBeVisible()
await page.getByRole("button", { name: "Place order" }).click()
})
await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible()
})
test("wizard validates each step before proceeding", async ({ page }) => {
await page.goto("/checkout")
await page.getByRole("button", { name: "Continue" }).click()
await expect(page.getByRole("heading", { name: "Shipping" })).toBeVisible()
await expect(page.getByText("Address is required")).toBeVisible()
})
test("wizard supports going back without losing data", async ({ page }) => {
await page.goto("/checkout")
await page.getByLabel("Address").fill("123 Main St")
await page.getByLabel("City").fill("Portland")
await page.getByLabel("State").selectOption("OR")
await page.getByLabel("ZIP code").fill("97201")
await page.getByRole("button", { name: "Continue" }).click()
await page.getByRole("button", { name: "Back" }).click()
await expect(page.getByLabel("Address")).toHaveValue("123 Main St")
await expect(page.getByLabel("City")).toHaveValue("Portland")
})
自动补全与输入提示字段
使用场景:测试搜索字段、地址查找、提及选择器,或任何在用户输入时显示建议的输入框。 避免场景:字段是普通文本输入框,没有建议功能。
TypeScript
import { test, expect } from "@playwright/test"
test("select from auto-complete suggestions", async ({ page }) => {
await page.goto("/search")
const searchField = page.getByRole("combobox", { name: "Search" })
// 以足够慢的速度输入,让建议出现
// pressSequentially 模拟真实按键(触发 keydown/keyup/input 事件)
await searchField.pressSequentially("playw", { delay: 100 })
// 等待建议列表出现
const suggestions = page.getByRole("listbox")
await expect(suggestions).toBeVisible()
// 选择特定建议
await suggestions.getByRole("option", { name: "Playwright Testing" }).click()
// 验证选择已填入字段
await expect(searchField).toHaveValue("Playwright Testing")
})
test("auto-complete with API-driven suggestions", async ({ page }) => {
await page.goto("/address-form")
const addressField = page.getByLabel("Address")
await addressField.pressSequentially("123 Ma", { delay: 50 })
// 等待 API 驱动的建议列表
const responsePromise = page.waitForResponse("**/api/address-suggest*")
await responsePromise
await page.getByRole("option", { name: /123 Main St/ }).click()
// 验证依赖字段已自动填充
await expect(page.getByLabel("City")).toHaveValue("Portland")
await expect(page.getByLabel("State")).toHaveValue("OR")
await expect(page.getByLabel("ZIP code")).toHaveValue("97201")
})
test("dismiss auto-complete and use custom value", async ({ page }) => {
await page.goto("/tags")
const tagInput = page.getByLabel("Add tag")
await tagInput.pressSequentially("custom-tag")
// 按 Escape 关闭建议
await tagInput.press("Escape")
await expect(page.getByRole("listbox")).not.toBeVisible()
// 按 Enter 提交自定义值
await tagInput.press("Enter")
await expect(page.getByText("custom-tag")).toBeVisible()
})
JavaScript
const { test, expect } = require("@playwright/test")
test("select from auto-complete suggestions", async ({ page }) => {
await page.goto("/search")
const searchField = page.getByRole("combobox", { name: "Search" })
await searchField.pressSequentially("playw", { delay: 100 })
const suggestions = page.getByRole("listbox")
await expect(suggestions).toBeVisible()
await suggestions.getByRole("option", { name: "Playwright Testing" }).click()
await expect(searchField).toHaveValue("Playwright Testing")
})
test("auto-complete with API-driven suggestions", async ({ page }) => {
await page.goto("/address-form")
const addressField = page.getByLabel("Address")
await addressField.pressSequentially("123 Ma", { delay: 50 })
const responsePromise = page.waitForResponse("**/api/address-suggest*")
await responsePromise
await page.getByRole("option", { name: /123 Main St/ }).click()
await expect(page.getByLabel("City")).toHaveValue("Portland")
await expect(page.getByLabel("State")).toHaveValue("OR")
await expect(page.getByLabel("ZIP code")).toHaveValue("97201")
})
test("dismiss auto-complete and use custom value", async ({ page }) => {
await page.goto("/tags")
const tagInput = page.getByLabel("Add tag")
await tagInput.pressSequentially("custom-tag")
await tagInput.press("Escape")
await expect(page.getByRole("listbox")).not.toBeVisible()
await tagInput.press("Enter")
await expect(page.getByText("custom-tag")).toBeVisible()
})
动态表单——条件字段
使用场景:表单字段根据其他字段的值显示、隐藏或变化。 避免场景:所有字段始终可见。使用基本表单填写模式。
TypeScript
import { test, expect } from "@playwright/test"
test("conditional fields appear based on selection", async ({ page }) => {
await page.goto("/insurance/apply")
// 选择"Business"会显示额外字段
await page.getByLabel("Account type").selectOption("business")
// 等待条件字段出现
await expect(page.getByLabel("Company name")).toBeVisible()
await expect(page.getByLabel("Tax ID")).toBeVisible()
await page.getByLabel("Company name").fill("Acme Corp")
await page.getByLabel("Tax ID").fill("12-3456789")
// 切回"Personal"会隐藏它们
await page.getByLabel("Account type").selectOption("personal")
await expect(page.getByLabel("Company name")).not.toBeVisible()
await expect(page.getByLabel("Tax ID")).not.toBeVisible()
})
test("checkbox toggles additional section", async ({ page }) => {
await page.goto("/shipping")
// "Use different billing address"会显示账单地址字段
await page.getByLabel("Use different billing address").check()
const billingSection = page.getByRole("group", { name: "Billing address" })
await expect(billingSection).toBeVisible()
await billingSection.getByLabel("Street").fill("456 Oak Ave")
await billingSection.getByLabel("City").fill("Seattle")
// 取消勾选会隐藏该区域
await page.getByLabel("Use different billing address").uncheck()
await expect(billingSection).not.toBeVisible()
})
test("dependent dropdown chains", async ({ page }) => {
await page.goto("/location-picker")
// 选择国家会填充州下拉列表
await page.getByLabel("Country").selectOption("US")
// 等待依赖的下拉列表填充
const stateDropdown = page.getByLabel("State")
await expect(stateDropdown.getByRole("option")).not.toHaveCount(0)
await stateDropdown.selectOption("CA")
// 选择州会填充城市下拉列表
const cityDropdown = page.getByLabel("City")
await expect(cityDropdown.getByRole("option")).not.toHaveCount(0)
await cityDropdown.selectOption({ label: "Los Angeles" })
})
JavaScript
const { test, expect } = require("@playwright/test")
test("conditional fields appear based on selection", async ({ page }) => {
await page.goto("/insurance/apply")
await page.getByLabel("Account type").selectOption("business")
await expect(page.getByLabel("Company name")).toBeVisible()
await expect(page.getByLabel("Tax ID")).toBeVisible()
await page.getByLabel("Company name").fill("Acme Corp")
await page.getByLabel("Tax ID").fill("12-3456789")
await page.getByLabel("Account type").selectOption("personal")
await expect(page.getByLabel("Company name")).not.toBeVisible()
await expect(page.getByLabel("Tax ID")).not.toBeVisible()
})
test("checkbox toggles additional section", async ({ page }) => {
await page.goto("/shipping")
await page.getByLabel("Use different billing address").check()
const billingSection = page.getByRole("group", { name: "Billing address" })
await expect(billingSection).toBeVisible()
await billingSection.getByLabel("Street").fill("456 Oak Ave")
await billingSection.getByLabel("City").fill("Seattle")
await page.getByLabel("Use different billing address").uncheck()
await expect(billingSection).not.toBeVisible()
})
test("dependent dropdown chains", async ({ page }) => {
await page.goto("/location-picker")
await page.getByLabel("Country").selectOption("US")
const stateDropdown = page.getByLabel("State")
await expect(stateDropdown.getByRole("option")).not.toHaveCount(0)
await stateDropdown.selectOption("CA")
const cityDropdown = page.getByLabel("City")
await expect(cityDropdown.getByRole("option")).not.toHaveCount(0)
await cityDropdown.selectOption({ label: "Los Angeles" })
})
表单提交与响应处理
使用场景:测试表单提交后的行为——成功消息、重定向、服务器返回的错误响应以及提交过程中的加载状态。 避免场景:你只关心客户端校验。将提交测试与校验分开。
TypeScript
import { test, expect } from "@playwright/test"
test("successful form submission shows confirmation", async ({ page }) => {
await page.goto("/contact")
await page.getByLabel("Name").fill("Jane Doe")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Message").fill("Hello from Playwright")
// 在提交时等待 API 响应
const responsePromise = page.waitForResponse("**/api/contact")
await page.getByRole("button", { name: "Send message" }).click()
const response = await responsePromise
expect(response.status()).toBe(200)
await expect(page.getByText("Message sent successfully")).toBeVisible()
})
test("form submission shows server-side validation errors", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("Email").fill("taken@example.com")
await page.getByLabel("Password", { exact: true }).fill("ValidP@ss1")
await page.getByRole("button", { name: "Register" }).click()
// 服务器返回 409——邮箱已被注册
await expect(page.getByText("An account with this email already exists")).toBeVisible()
})
test("form shows loading state during submission", async ({ page }) => {
await page.goto("/contact")
await page.getByLabel("Name").fill("Jane")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Message").fill("Test")
await page.getByRole("button", { name: "Send message" }).click()
// 提交过程中按钮应禁用
await expect(page.getByRole("button", { name: /Sending/ })).toBeDisabled()
// 完成后,按钮恢复为可用状态
await expect(page.getByRole("button", { name: "Send message" })).toBeEnabled()
})
test("form redirects after successful submission", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("user@example.com")
await page.getByLabel("Password").fill("password123")
await page.getByRole("button", { name: "Sign in" }).click()
// 验证重定向
await page.waitForURL("/dashboard")
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})
JavaScript
const { test, expect } = require("@playwright/test")
test("successful form submission shows confirmation", async ({ page }) => {
await page.goto("/contact")
await page.getByLabel("Name").fill("Jane Doe")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Message").fill("Hello from Playwright")
const responsePromise = page.waitForResponse("**/api/contact")
await page.getByRole("button", { name: "Send message" }).click()
const response = await responsePromise
expect(response.status()).toBe(200)
await expect(page.getByText("Message sent successfully")).toBeVisible()
})
test("form submission shows server-side validation errors", async ({ page }) => {
await page.goto("/register")
await page.getByLabel("Email").fill("taken@example.com")
await page.getByLabel("Password", { exact: true }).fill("ValidP@ss1")
await page.getByRole("button", { name: "Register" }).click()
await expect(page.getByText("An account with this email already exists")).toBeVisible()
})
test("form shows loading state during submission", async ({ page }) => {
await page.goto("/contact")
await page.getByLabel("Name").fill("Jane")
await page.getByLabel("Email").fill("jane@example.com")
await page.getByLabel("Message").fill("Test")
await page.getByRole("button", { name: "Send message" }).click()
await expect(page.getByRole("button", { name: /Sending/ })).toBeDisabled()
await expect(page.getByRole("button", { name: "Send message" })).toBeEnabled()
})
test("form redirects after successful submission", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("user@example.com")
await page.getByLabel("Password").fill("password123")
await page.getByRole("button", { name: "Sign in" }).click()
await page.waitForURL("/dashboard")
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})
表单重置测试
使用场景:测试"清空表单"或"重置"功能,验证字段是否恢复为默认值。 避免场景:表单没有重置机制。
TypeScript
import { test, expect } from "@playwright/test"
test("reset button clears all fields to defaults", async ({ page }) => {
await page.goto("/settings")
// 修改字段默认值
await page.getByLabel("Display name").fill("New Name")
await page.getByLabel("Theme").selectOption("dark")
await page.getByLabel("Notifications").uncheck()
// 点击重置
await page.getByRole("button", { name: "Reset" }).click()
// 验证字段已恢复为原始值
await expect(page.getByLabel("Display name")).toHaveValue("")
await expect(page.getByLabel("Theme")).toHaveValue("light")
await expect(page.getByLabel("Notifications")).toBeChecked()
})
test("confirmation dialog before resetting a dirty form", async ({ page }) => {
await page.goto("/editor")
await page.getByLabel("Title").fill("Draft post")
// 重置触发确认对话框
page.on("dialog", (dialog) => dialog.accept())
await page.getByRole("button", { name: "Discard changes" }).click()
await expect(page.getByLabel("Title")).toHaveValue("")
})
JavaScript
const { test, expect } = require("@playwright/test")
test("reset button clears all fields to defaults", async ({ page }) => {
await page.goto("/settings")
await page.getByLabel("Display name").fill("New Name")
await page.getByLabel("Theme").selectOption("dark")
await page.getByLabel("Notifications").uncheck()
await page.getByRole("button", { name: "Reset" }).click()
await expect(page.getByLabel("Display name")).toHaveValue("")
await expect(page.getByLabel("Theme")).toHaveValue("light")
await expect(page.getByLabel("Notifications")).toBeChecked()
})
test("confirmation dialog before resetting a dirty form", async ({ page }) => {
await page.goto("/editor")
await page.getByLabel("Title").fill("Draft post")
page.on("dialog", (dialog) => dialog.accept())
await page.getByRole("button", { name: "Discard changes" }).click()
await expect(page.getByLabel("Title")).toHaveValue("")
})
决策指南
| 场景 | 方法 | 关键 API |
|---|---|---|
| 标准文本输入 | fill()(先清空再输入) |
page.getByLabel('Name').fill('Jane') |
| 需要按键事件(自动补全) | pressSequentially() 带延迟 |
locator.pressSequentially('text', { delay: 100 }) |
原生 <select> 下拉 |
selectOption() 按值或标签 |
locator.selectOption('US') 或 { label: 'United States' } |
| 自定义下拉(ARIA listbox) | 点击触发,然后选择 option 角色 | getByRole('option', { name: '...' }).click() |
| 复选框 | check() / uncheck()(幂等) |
locator.check()——即使已勾选也可安全调用 |
| 单选按钮 | 在目标单选按钮上调用 check() |
page.getByLabel('Express').check() |
| 日期输入(原生) | fill() 使用 ISO 格式 |
locator.fill('2025-03-15') |
| 日期选择器(第三方) | 点击打开,导航,选择日期 | getByRole('gridcell', { name: '15' }).click() |
| 校验错误 | 提交,然后断言错误文本 | expect(page.getByText('Required')).toBeVisible() |
| 多步骤向导 | 每步使用 test.step(),断言标题 |
await test.step('Step 1', async () => { ... }) |
| 条件/动态字段 | 更改触发字段,断言新字段可见性 | expect(locator).toBeVisible() / .not.toBeVisible() |
| 表单提交 | waitForResponse + 点击提交 |
在点击前注册响应监听器 |
| 自动补全 | pressSequentially(),等待 listbox,选择选项 |
getByRole('option', { name }).click() |
| 表单重置 | 点击重置,断言默认值 | expect(locator).toHaveValue('') |
反模式
| 不要这样做 | 问题 | 应改为 |
|---|---|---|
await page.getByLabel('Name').type('Jane') |
type() 会追加到已有内容后面,不会先清空 |
await page.getByLabel('Name').fill('Jane') |
await page.getByLabel('Agree').click() |
click() 会切换状态——如果已勾选,则会取消勾选 |
await page.getByLabel('Agree').check() |
await page.fill('#email', 'test@test.com') |
CSS 选择器很脆弱 | await page.getByLabel('Email').fill('test@test.com') |
await page.selectOption('select', 'US') 未指定标签 |
默认选中页面上第一个 <select>,有歧义 |
await page.getByLabel('Country').selectOption('US') |
| 在一个测试中测试所有无效输入 | 测试变得庞大、缓慢且难以调试 | 每个校验规则或相关规则分组各用一个测试 |
expect(await input.inputValue()).toBe('Jane') |
只解析一次——无重试。存在竞态条件。 | await expect(input).toHaveValue('Jane') |
使用 page.evaluate() 填写字段 |
绕过事件处理器(不会触发 input、change 事件) |
使用 fill() 或 pressSequentially() |
| 填写前未等待条件字段 | fill() 在隐藏/已分离的元素上会失败 |
先 await expect(field).toBeVisible() |
| 选择下拉后硬编码等待 | waitForTimeout(500) 不稳定且慢 |
等待依赖元素出现 |
| 跳过服务端校验测试 | 客户端校验可以被绕过 | 同时测试客户端 UX 和服务器响应 |
故障排除
fill() 无效果,或清空了但未输入
原因:输入字段使用的是 contenteditable div(富文本编辑器),而非真正的 <input> 或 <textarea>。
// 检查是否为 contenteditable
const isContentEditable = await page
.getByTestId("editor")
.evaluate((el) => el.getAttribute("contenteditable"))
// 对于 contenteditable,使用 pressSequentially 或 type
if (isContentEditable) {
await page.getByTestId("editor").click()
await page.getByTestId("editor").pressSequentially("Hello world")
}
日期选择器不接受 fill() 的值
原因:第三方日期选择器通常在隐藏输入之上渲染自定义 UI。fill() 设置了隐藏输入的值,但 UI 不会更新。
// 改为与日期选择器 UI 交互
await page.getByLabel("Date").click() // 打开选择器
await page.getByRole("button", { name: "Next month" }).click()
await page.getByRole("gridcell", { name: "15" }).click()
// 或者,如果库在 change 事件时读取输入值:
await page.getByLabel("Date").fill("2025-06-15")
await page.getByLabel("Date").dispatchEvent("change")