# 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 `