# 表单与校验 > **使用场景**:测试表单填写、提交、校验信息、多步骤向导、动态字段和自动补全交互。 > **前置知识**:[core/locators.md](locators.md)、[core/assertions-and-waiting.md](assertions-and-waiting.md) ## 快速参考 ```typescript // 文本输入 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** ```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") // 原生 `、``、`` 或第三方日期选择器。 **避免场景**:日期选择器是普通文本字段,没有特殊输入类型。直接使用 `fill()` 即可。 **TypeScript** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```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 })` | | 原生 ``,有歧义 | `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(富文本编辑器),而非真正的 `` 或 `