# Forms and Validation > **When to use**: Testing form filling, submission, validation messages, multi-step wizards, dynamic fields, and auto-complete interactions. > **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md) ## Quick Reference ```typescript // Text input await page.getByLabel("Name").fill("Jane Doe") // Select dropdown await page.getByLabel("Country").selectOption("US") await page.getByLabel("Country").selectOption({ label: "United States" }) // Checkbox and radio await page.getByLabel("Remember me").check() await page.getByLabel("Express shipping").click() // Date input await page.getByLabel("Start date").fill("2025-03-15") // Clear a field await page.getByLabel("Name").clear() // Submit await page.getByRole("button", { name: "Submit" }).click() // Verify validation error await expect(page.getByText("Email is required")).toBeVisible() ``` ## Patterns ### Filling Basic Form Fields **Use when**: Testing any form with standard HTML inputs — text, email, password, number, textarea, select, checkbox, radio. **Avoid when**: Never. This is the foundation pattern. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("fill and submit a registration form", async ({ page }) => { await page.goto("/register") // Text inputs — use fill() which clears first, not 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!") // Textarea await page.getByLabel("Bio").fill("Software engineer with 10 years of experience.") // Number input await page.getByLabel("Age").fill("32") // Native `, ``, ``, or third-party date pickers. **Avoid when**: The date picker is a simple text field with no special input type. Just use `fill()`. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("fill native date and time inputs", async ({ page }) => { await page.goto("/booking") // Native date input — use ISO format YYYY-MM-DD await page.getByLabel("Check-in date").fill("2025-06-15") await expect(page.getByLabel("Check-in date")).toHaveValue("2025-06-15") // Native time input — use HH:MM format await page.getByLabel("Arrival time").fill("14:30") // datetime-local — use YYYY-MM-DDTHH:MM format await page.getByLabel("Event start").fill("2025-06-15T09:00") }) test("interact with a third-party date picker", async ({ page }) => { await page.goto("/booking") // Click to open the date picker await page.getByLabel("Departure date").click() // Navigate months if needed await page.getByRole("button", { name: "Next month" }).click() // Select a specific day await page.getByRole("gridcell", { name: "20" }).click() // Verify the selected date appears in the input 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/) }) ``` ### Required Field Validation **Use when**: Testing that the form shows appropriate error messages when required fields are empty. **Avoid when**: You only care about the happy path. Validation tests should complement, not replace, success path tests. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("shows validation errors for empty required fields", async ({ page }) => { await page.goto("/contact") // Submit without filling anything await page.getByRole("button", { name: "Send message" }).click() // Verify all required field errors appear await expect(page.getByText("Name is required")).toBeVisible() await expect(page.getByText("Email is required")).toBeVisible() await expect(page.getByText("Message is required")).toBeVisible() // Verify the form was NOT submitted (still on the same page) await expect(page).toHaveURL(/\/contact/) }) test("clears validation errors when fields are filled", async ({ page }) => { await page.goto("/contact") // Trigger errors await page.getByRole("button", { name: "Send message" }).click() await expect(page.getByText("Name is required")).toBeVisible() // Fill the field — error should disappear await page.getByLabel("Name").fill("Jane Doe") // Use tab or click away to trigger blur validation 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() // Check for native validation message via the :invalid pseudo-class 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() }) ``` ### Format Validation and Custom Rules **Use when**: Testing email format, phone number format, password strength, and business-specific validation rules. **Avoid when**: The validation is purely server-side with no client-side feedback. Test via API instead. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("validates email format", async ({ page }) => { await page.goto("/register") const emailField = page.getByLabel("Email") // Invalid formats 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() } // Valid format clears the error 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 }) // Too short await passwordField.fill("Ab1!") await passwordField.blur() await expect(page.getByText("At least 8 characters")).toBeVisible() // Missing uppercase await passwordField.fill("abcdefg1!") await passwordField.blur() await expect(page.getByText("At least one uppercase letter")).toBeVisible() // Strong password — all checks pass 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() }) ``` ### Multi-Step Forms and Wizards **Use when**: The form spans multiple pages or steps, with next/previous navigation and per-step validation. **Avoid when**: The form is a single page. Use the basic form filling pattern. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("complete a multi-step checkout wizard", async ({ page }) => { await page.goto("/checkout") // Step 1: Shipping 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() }) // Step 2: Payment 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() }) // Step 3: Review await test.step("review and confirm order", async () => { await expect(page.getByRole("heading", { name: "Review" })).toBeVisible() // Verify data from previous steps is shown await expect(page.getByText("123 Main St")).toBeVisible() await expect(page.getByText("ending in 4242")).toBeVisible() await page.getByRole("button", { name: "Place order" }).click() }) // Confirmation await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible() }) test("wizard validates each step before proceeding", async ({ page }) => { await page.goto("/checkout") // Try to skip step 1 without filling required fields await page.getByRole("button", { name: "Continue" }).click() // Should stay on step 1 with validation errors 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") // Fill step 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() // Go back from step 2 await page.getByRole("button", { name: "Back" }).click() // Verify step 1 data is preserved 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") }) ``` ### Auto-Complete and Typeahead Fields **Use when**: Testing search fields, address lookups, mention pickers, or any input that shows suggestions as the user types. **Avoid when**: The field is a plain text input with no suggestions. **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" }) // Type slowly enough for suggestions to appear // pressSequentially simulates real keystrokes (triggers keydown/keyup/input events) await searchField.pressSequentially("playw", { delay: 100 }) // Wait for the suggestion list to appear const suggestions = page.getByRole("listbox") await expect(suggestions).toBeVisible() // Select a specific suggestion await suggestions.getByRole("option", { name: "Playwright Testing" }).click() // Verify the selection populated the field 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 }) // Wait for the API-driven suggestion list const responsePromise = page.waitForResponse("**/api/address-suggest*") await responsePromise await page.getByRole("option", { name: /123 Main St/ }).click() // Verify dependent fields were auto-populated 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") // Dismiss suggestions with Escape await tagInput.press("Escape") await expect(page.getByRole("listbox")).not.toBeVisible() // Submit custom value with 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() }) ``` ### Dynamic Forms — Conditional Fields **Use when**: Form fields appear, disappear, or change based on the value of other fields. **Avoid when**: All fields are always visible. Use the basic form filling pattern. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("conditional fields appear based on selection", async ({ page }) => { await page.goto("/insurance/apply") // Selecting "Business" shows additional fields await page.getByLabel("Account type").selectOption("business") // Wait for conditional fields to appear 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") // Switching back to "Personal" hides them 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") // "Different billing address" reveals billing fields 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") // Unchecking hides the section await page.getByLabel("Use different billing address").uncheck() await expect(billingSection).not.toBeVisible() }) test("dependent dropdown chains", async ({ page }) => { await page.goto("/location-picker") // Country selection populates the state dropdown await page.getByLabel("Country").selectOption("US") // Wait for the dependent dropdown to be populated const stateDropdown = page.getByLabel("State") await expect(stateDropdown.getByRole("option")).not.toHaveCount(0) await stateDropdown.selectOption("CA") // State selection populates the city dropdown 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" }) }) ``` ### Form Submission and Response Handling **Use when**: Testing what happens after a form is submitted — success messages, redirects, error responses from the server, and loading states during submission. **Avoid when**: You only care about client-side validation. Test submission separately from validation. **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") // Wait for the API response during submission 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() // Server responds with a 409 — email already taken 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() // Button should be disabled during submission await expect(page.getByRole("button", { name: /Sending/ })).toBeDisabled() // After completion, button returns to normal 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() // Verify redirect 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() }) ``` ### Form Reset Testing **Use when**: Testing "clear form" or "reset" functionality, verifying that fields return to their default values. **Avoid when**: The form has no reset mechanism. **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("reset button clears all fields to defaults", async ({ page }) => { await page.goto("/settings") // Change fields from defaults await page.getByLabel("Display name").fill("New Name") await page.getByLabel("Theme").selectOption("dark") await page.getByLabel("Notifications").uncheck() // Click reset await page.getByRole("button", { name: "Reset" }).click() // Verify fields returned to original values 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") // Reset triggers a confirmation dialog 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("") }) ``` ## Decision Guide | Scenario | Approach | Key API | | ------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------------ | | Standard text input | `fill()` (clears, then types) | `page.getByLabel('Name').fill('Jane')` | | Need keystroke events (autocomplete) | `pressSequentially()` with delay | `locator.pressSequentially('text', { delay: 100 })` | | Native `` on page; ambiguous | `await page.getByLabel('Country').selectOption('US')` | | Testing every invalid input in one test | Test becomes huge, slow, and hard to debug | One test per validation rule or group related rules | | `expect(await input.inputValue()).toBe('Jane')` | Resolves once — no retry. Race condition. | `await expect(input).toHaveValue('Jane')` | | Filling fields with `page.evaluate()` | Bypasses event handlers (no `input`, `change` events fire) | Use `fill()` or `pressSequentially()` | | Not waiting for conditional fields before filling | `fill()` fails on hidden/detached elements | `await expect(field).toBeVisible()` first | | Hardcoding wait after selecting a dropdown | `waitForTimeout(500)` is flaky and slow | Wait for the dependent element to appear | | Skipping server-side validation tests | Client-side validation can be bypassed | Test both client-side UX and server response | ## Troubleshooting ### `fill()` does nothing or clears but doesn't type **Cause**: The input field uses a contenteditable div (rich text editors), not a real `` or `