Core Playwright Concepts
Handling different types of elements (buttons, inputs, dropdowns)
This lesson covers how to interact with common web elements like buttons, inputs, and dropdowns in Playwright. Understanding these interactions is crucial because they form the foundation of most test scenarios, from simple form submissions to complex user flows. You’ll reach for these techniques whenever you need to simulate real user actions in your tests.
Locating Elements: The First Step
Before interacting with any element, you must locate it on the page. Playwright provides multiple locator strategies, such as `getByRole`, `getByText`, and `getByTestId`, to help you find elements reliably. The `getByRole` strategy is particularly powerful because it aligns with how assistive technologies identify elements, making your tests more resilient to changes in the DOM structure. For example, a button can be located by its role (`button`) and accessible name, which ensures your test doesn’t break if the button’s class or ID changes. This approach also encourages writing tests that are more accessible by default. Always prefer semantic locators over CSS or XPath when possible, as they are less brittle and more maintainable. If an element lacks a clear role or text, consider adding a `data-testid` attribute to it, which provides a stable hook for your tests without affecting production behavior.
// Locate a button by its role and accessible name
const submitButton = page.getByRole('button', { name: 'Submit' });
// Locate an input field by its label text
const usernameInput = page.getByLabel('Username');
// Locate an element by test ID (useful for dynamic or complex elements)
const menuItem = page.getByTestId('user-dropdown-menu');
// Assert the elements are visible before interacting
await expect(submitButton).toBeVisible();
await expect(usernameInput).toBeVisible();Interacting with Buttons
Buttons are one of the simplest yet most common elements you’ll interact with in tests. Playwright’s `click()` method simulates a user clicking a button, which can trigger form submissions, navigation, or other actions. However, clicking a button isn’t always as straightforward as it seems. For instance, if the button is hidden or disabled, the click will fail, so it’s good practice to assert its state before interacting. Additionally, some buttons may trigger asynchronous actions, like API calls, so you’ll need to wait for the expected outcome (e.g., a navigation or a UI update) before proceeding. Playwright’s auto-waiting mechanism helps here, as it waits for the element to be actionable (visible, enabled, and stable) before performing the click. If the button is part of a dynamic UI, like a modal or dropdown, you may need to chain actions, such as opening the modal first and then clicking the button inside it.
// Click a button and wait for navigation
await page.getByRole('button', { name: 'Login' }).click();
// Wait for the URL to change after clicking
await expect(page).toHaveURL('/dashboard');
// Handle a button that triggers an API call (wait for a UI update)
const saveButton = page.getByRole('button', { name: 'Save' });
await saveButton.click();
// Wait for a success message to appear
await expect(page.getByText('Changes saved successfully')).toBeVisible();Filling Input Fields
Input fields are another fundamental element in web forms, and Playwright provides the `fill()` method to quickly populate them with text. Unlike `type()`, which simulates each keystroke (useful for testing autocomplete or input events), `fill()` directly sets the value of the input, making it faster and more reliable for most cases. However, there are scenarios where `fill()` isn’t sufficient. For example, if the input has validation that triggers on each keystroke, you might need to use `type()` to simulate real user behavior. Additionally, some inputs, like date pickers or custom dropdowns, may require clicking the input first to open a picker or dropdown before selecting a value. Playwright’s `locator.focus()` can be useful here to ensure the input is ready to receive text. Always clear the input field before filling it if there’s a chance it already contains a value, as this ensures your test starts from a clean state.
// Fill a text input field
await page.getByLabel('Email').fill('user@example.com');
// Clear and fill a field that might have a default value
const passwordInput = page.getByLabel('Password');
await passwordInput.clear();
await passwordInput.fill('securePassword123');
// Use type() to simulate keystrokes (e.g., for autocomplete)
await page.getByLabel('Search').type('Playwright', { delay: 100 });
// Handle a date input by focusing and filling
const dateInput = page.getByLabel('Birthdate');
await dateInput.focus();
await dateInput.fill('2023-01-01');Selecting Options from Dropdowns
Dropdowns (or `<select>` elements) are common in forms and require a different approach than buttons or inputs. Playwright provides the `selectOption()` method to choose one or more options from a dropdown. This method works with both single-select and multi-select dropdowns, and it can accept values, labels, or indices to identify the option. However, not all dropdowns are implemented using `<select>` elements. Many modern UIs use custom dropdowns built with `<div>` or `<ul>` elements, which require a different interaction pattern. For these, you’ll typically need to click the dropdown to open it, then click the desired option. Playwright’s `getByRole('option')` can help locate options in custom dropdowns, but you may need to chain actions, such as waiting for the dropdown to open before selecting an option. Always verify the selected option after interaction to ensure the test behaves as expected.
// Select an option from a standard <select> dropdown
await page.getByLabel('Country').selectOption('Canada');
// Select multiple options (for multi-select dropdowns)
await page.getByLabel('Languages').selectOption(['English', 'French']);
// Handle a custom dropdown (click to open, then select)
await page.getByRole('button', { name: 'Choose a plan' }).click();
await page.getByRole('option', { name: 'Premium' }).click();
// Verify the selected option
await expect(page.getByLabel('Country')).toHaveValue('Canada');Handling Dynamic and Conditional Elements
In real-world applications, elements often appear or disappear based on user actions, API responses, or other conditions. Playwright’s auto-waiting helps with this, but there are cases where you need to explicitly wait for an element to be ready. For example, a dropdown might take a moment to load options after being opened, or a button might become enabled only after filling out a form. Playwright provides methods like `waitForSelector()` and `waitForFunction()` to handle these scenarios, but the preferred approach is to use assertions like `toBeVisible()` or `toBeEnabled()`, which implicitly wait for the condition to be met. This makes your tests more readable and less prone to flakiness. Additionally, some elements may be conditionally rendered, such as a success message that only appears after a form submission. In these cases, you can use `expect(locator).toBeVisible()` to wait for the element to appear before interacting with it. Always consider the state of the element before interacting, as this ensures your tests are robust and reliable.
// Wait for a dynamic element to appear
const successMessage = page.getByText('Account created successfully');
await expect(successMessage).toBeVisible();
// Wait for a button to become enabled after filling a form
const submitButton = page.getByRole('button', { name: 'Submit' });
await expect(submitButton).toBeEnabled();
// Handle a dropdown that loads options dynamically
await page.getByRole('button', { name: 'Select a city' }).click();
const option = page.getByRole('option', { name: 'Toronto' });
await expect(option).toBeVisible();
await option.click();
// Use waitForFunction for complex conditions
await page.waitForFunction(() => {
const element = document.querySelector('.dynamic-element');
return element && element.offsetHeight > 0;
});Key points
- Always prefer semantic locators like `getByRole` or `getByLabel` over CSS or XPath to make your tests more resilient to DOM changes.
- Use `fill()` for quickly setting input values, but switch to `type()` when you need to simulate keystrokes or test input events.
- For dropdowns, use `selectOption()` for standard `<select>` elements, but chain actions like `click()` for custom dropdowns built with `<div>` or `<ul>`.
- Playwright’s auto-waiting ensures elements are actionable before interaction, but explicitly wait for dynamic elements using assertions like `toBeVisible()`.
- Clear input fields before filling them to avoid test flakiness caused by leftover values from previous test runs.
- Verify the state of elements (e.g., enabled, visible) before interacting with them to handle conditional or dynamic UIs.
- For buttons, always wait for the expected outcome (e.g., navigation, UI update) after clicking to ensure the action completed successfully.
- Use `data-testid` attributes as a last resort for locating elements when semantic attributes are unavailable or insufficient.
Common mistakes
- Mistake: Using fixed selectors like `div:nth-child(2)` for buttons/inputs. Why it's wrong: These selectors break when the DOM structure changes. Fix: Prefer stable attributes like `data-testid`, `role`, or `aria-label`.
- Mistake: Assuming dropdowns are always `<select>` elements. Why it's wrong: Many modern UIs use custom dropdowns built with `<div>` or `<ul>/<li>`. Fix: Inspect the actual implementation and use `locator('role=combobox')` or similar.
- Mistake: Not waiting for elements to be visible/interactable before acting on them. Why it's wrong: Playwright may try to click an element before it’s ready, causing flaky tests. Fix: Use `await expect(locator).toBeVisible()` or `waitFor()`.
- Mistake: Hardcoding input values in tests. Why it's wrong: Tests fail when requirements change (e.g., password length rules). Fix: Use dynamic data with `faker` or environment variables.
- Mistake: Using `click()` on disabled buttons/inputs. Why it's wrong: Playwright throws errors if the element isn’t interactable. Fix: Check `isDisabled()` first or verify the expected behavior (e.g., no action should occur).
Interview questions
How do you locate a button element in Playwright and click it?
In Playwright, you locate a button using one of its built-in selectors like `getByRole`, `getByText`, or `getByTestId`. The best practice is to use `getByRole('button', { name: 'Submit' })` because it’s accessible and matches the button’s semantic role. After locating it, you call `.click()` to interact with it. Here’s an example: `await page.getByRole('button', { name: 'Submit' }).click();`. This ensures the test is reliable and follows accessibility standards, making it less likely to break if the DOM structure changes.
How do you fill out a text input field in Playwright, and why is it important to clear it first?
To fill a text input in Playwright, you first locate it using a selector like `getByLabel` or `getByPlaceholder`, then call `.fill('text')`. Clearing the input first is important because some fields might have default values or leftover text from previous interactions. If you don’t clear it, the new text might append to the old one, causing unexpected behavior. For example: `await page.getByLabel('Username').fill('testuser');`. If the field isn’t empty, you can use `.clear()` before filling it, like this: `await page.getByLabel('Username').clear().fill('testuser');`. This ensures the input contains only the value you intend.
How do you select an option from a dropdown menu in Playwright, and what are the two common approaches?
In Playwright, you can select an option from a dropdown in two main ways. The first is using `selectOption`, which works for standard `<select>` elements. For example: `await page.getByLabel('Country').selectOption('US');`. This is the simplest method because Playwright handles the underlying events. The second approach is to interact with the dropdown like a user would, by clicking it to open the list, then clicking the desired option. For example: `await page.getByLabel('Country').click(); await page.getByRole('option', { name: 'US' }).click();`. The first method is faster and more reliable for native dropdowns, while the second is necessary for custom dropdowns that don’t use `<select>` elements.
Compare the `click()` and `dblclick()` methods in Playwright. When would you use each, and what are the risks of misusing them?
The `click()` method simulates a single mouse click, while `dblclick()` simulates a double-click. You’d use `click()` for standard interactions like submitting a form or opening a menu, as most UI actions require just one click. For example: `await page.getByRole('button', { name: 'Save' }).click();`. Double-clicks are rare but used for actions like selecting a word in a text editor or opening a file in a desktop-like UI. For example: `await page.getByText('Document').dblclick();`. Misusing these methods can cause flaky tests—using `dblclick()` when `click()` is expected might trigger unintended actions, while using `click()` twice instead of `dblclick()` could fail to trigger the correct behavior. Always match the method to the expected user interaction.
How do you handle dynamic dropdowns that load options asynchronously in Playwright, and why is waiting important?
For dynamic dropdowns that load options after a delay, you must wait for the options to appear before selecting one. Playwright provides auto-waiting for most actions, but you can explicitly wait using `waitForSelector` or `waitForResponse`. For example, if clicking a dropdown triggers an API call, you’d wait for the response: `await page.getByLabel('City').click(); await page.waitForResponse('**/cities'); await page.getByRole('option', { name: 'New York' }).click();`. Waiting is critical because if you try to select an option before it exists, the test will fail. Without waiting, the test might flake or miss the intended interaction entirely. Always ensure the UI is in the expected state before proceeding.
How do you test a custom dropdown that doesn’t use the `<select>` element, and what challenges might you face?
Custom dropdowns often use `<div>` or `<ul>` elements with JavaScript to mimic dropdown behavior. To test them, you first click the dropdown to open it, then locate and click the desired option. For example: `await page.getByTestId('custom-dropdown').click(); await page.getByRole('option', { name: 'Option 1' }).click();`. Challenges include flakiness due to timing issues—if the dropdown animates or loads data asynchronously, you might need to add explicit waits. Another challenge is ensuring the dropdown closes after selection, as custom implementations might not handle this automatically. You may need to verify the dropdown’s state after interaction, like checking if the selected value is displayed. Custom dropdowns also often lack semantic roles, so you might rely on `getByTestId` or `getByText`, which can make tests less maintainable if the UI changes.
Check yourself
1. How should you handle a custom dropdown built with `<div>` elements in Playwright?
- A.Use `locator('select')` and `selectOption()`
- B.Use `locator('div.dropdown')` with `click()` and then `click()` on the desired `<li>`
- C.Assume it’s a native `<select>` and use `selectOption()` directly
- D.Use `page.keyboard` to navigate with arrow keys
Show answer
B. Use `locator('div.dropdown')` with `click()` and then `click()` on the desired `<li>`
Option 1 is wrong because `selectOption()` only works for native `<select>` elements. Option 3 is incorrect for the same reason. Option 4 is unreliable for custom dropdowns. Option 2 is correct: interact with the dropdown container and then the specific option.
2. What’s the best way to ensure a button is clickable before interacting with it?
- A.Add a fixed `await page.waitForTimeout(2000)` delay
- B.Use `await expect(buttonLocator).toBeVisible()` followed by `click()`
- C.Assume Playwright’s auto-waiting is sufficient for all cases
- D.Use `buttonLocator.isEnabled()` in a loop until it returns `true`
Show answer
B. Use `await expect(buttonLocator).toBeVisible()` followed by `click()`
Option 0 is flaky and slow. Option 2 is incorrect because auto-waiting doesn’t account for visibility/state changes. Option 3 is unreliable for dynamic UIs. Option 1 is correct: `toBeVisible()` ensures the button is ready for interaction.
3. How would you test a form where the submit button is disabled until all fields are valid?
- A.Click the button without checking its state and expect an error
- B.Use `expect(buttonLocator).toBeDisabled()` before filling fields, then verify it becomes enabled
- C.Assume the button is always enabled and proceed with submission
- D.Use `page.evaluate()` to manually enable the button before clicking
Show answer
B. Use `expect(buttonLocator).toBeDisabled()` before filling fields, then verify it becomes enabled
Option 0 would fail the test. Option 2 is incorrect because it bypasses the intended behavior. Option 3 is wrong because it doesn’t test the disabled state. Option 1 is correct: verify the button’s state reflects the form’s validity.
4. What’s the most maintainable way to locate an input field with dynamic IDs?
- A.Use `page.locator('#input-' + dynamicId)` with string concatenation
- B.Use `data-testid` or `aria-label` attributes in the selector
- C.Rely on XPath like `//input[contains(@id, 'input-')]`
- D.Use `page.getByPlaceholder('Enter text')` if the placeholder is unique
Show answer
B. Use `data-testid` or `aria-label` attributes in the selector
Option 0 is brittle if `dynamicId` changes. Option 2 is fragile and hard to maintain. Option 3 is correct but less readable than semantic attributes. Option 1 is best: `data-testid` or `aria-label` are stable and explicit.
5. How should you handle a dropdown where options are loaded via API after clicking it?
- A.Assume options are present immediately and use `selectOption()`
- B.Click the dropdown, wait for a network request to complete, then select the option
- C.Use `page.waitForSelector()` with a long timeout for the option
- D.Mock the API response to return options instantly
Show answer
B. Click the dropdown, wait for a network request to complete, then select the option
Option 0 fails if options aren’t loaded. Option 2 is flaky without waiting for the API. Option 3 is correct but less precise than waiting for the API. Option 1 is best: combine UI interaction with network request waiting (e.g., `waitForResponse()`).