Resource library

QA How-To

How to Test a dropdown in Playwright (2026)

Learn playwright how to test a dropdown for native selects, custom listboxes, keyboard access, dynamic options, assertions, and stable TypeScript automation.

18 min read | 3,519 words

TL;DR

Use `selectOption()` for a native `<select>`, then assert the selected value or visible option. For a custom dropdown, interact through its accessible combobox and option roles, verify keyboard behavior, and never call `selectOption()` on a div-based widget.

Key Takeaways

  • Use `selectOption()` for a native `<select>`, then assert the selected value or visible option. For a custom dropdown, interact through its accessible combobox and option roles, verify keyboard behavior, and never call `selectOption()` on a div-based widget.
  • Prefer web-first assertions over manual polling where possible.
  • Create event and response waits before the action that triggers them.
  • Use accessible locators and stable product contracts.
  • Keep loops and waits bounded for CI safety.
  • Retain traces and targeted artifacts for useful failure diagnosis.

Use selectOption() for a native <select>, then assert the selected value or visible option. For a custom dropdown, interact through its accessible combobox and option roles, verify keyboard behavior, and never call selectOption() on a div-based widget. This guide answers playwright how to test a dropdown with production-oriented Playwright Test patterns for TypeScript.

The examples focus on reliable synchronization, accessible locators, useful assertions, and diagnostics. Replace the illustrative URLs and accessible names with contracts from your application, while keeping the event ordering and assertion strategy intact.

TL;DR

Decision Recommended approach Why
Native select selectOption() value and checked option
Custom listbox roles plus click or keyboard expanded state and chosen text
Autocomplete fill plus option selection query, results, and committed value

The core rule is simple: observe the outcome that matters, start event waits before the triggering action, and let Playwright's locator assertions retry. Never repair uncertainty with a fixed sleep.

1. playwright how to test a dropdown: Classify the dropdown first

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

2. Test a native select correctly

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

3. Select by value label or index

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

4. Assert the committed selection

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

5. Test a custom listbox

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

6. Cover keyboard interaction

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

7. Handle searchable comboboxes

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

8. Wait for dynamic options

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

9. Test disabled and invalid states

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

10. Avoid flaky locators

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

11. Build reusable dropdown helpers

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

12. Apply playwright how to test a dropdown

Testing native selects and custom dropdown widgets starts with an observable product contract. Write down the trigger, the state transition, and the evidence a user can see. A weak test proves that an automation command completed. A strong test proves that the application reached the intended state and remained usable. This distinction matters because Playwright's auto-waiting covers actionability, but it cannot decide what business outcome is correct.

For this part of the workflow, the useful Playwright surfaces include locator.selectOption(), getByRole("combobox"), getByRole("option"), toHaveValue(), toHaveText(), and keyboard actions. Choose the smallest API that expresses user intent, and pair it with a web-first assertion. Locators are evaluated again when Playwright retries, so prefer them over element handles or cached DOM values. Keep test data explicit, avoid arbitrary timeouts, and make every loop or network wait bounded. Those choices turn a demonstration into a test that can survive CI load and normal rendering variation.

Review the negative path as well as the happy path. Ask what happens when data arrives late, the control is disabled, navigation fails, or the user repeats the action. Assert only stable public behavior. Internal class names, framework-generated IDs, implementation-specific request timing, and exact pixel colors are poor primary contracts unless the product explicitly guarantees them. A failure should identify which contract broke, not merely report that a generic timeout expired.

Runnable TypeScript example

import { test, expect } from '@playwright/test';

test('selects a country from a native select', async ({ page }) => {
  await page.goto('https://example.com/profile');
  const country = page.getByLabel('Country');
  await country.selectOption({ label: 'India' });
  await expect(country).toHaveValue('IN');
  await expect(country.locator('option:checked')).toHaveText('India');
});

test('selects from an accessible custom listbox', async ({ page }) => {
  await page.goto('https://example.com/profile');
  await page.getByRole('combobox', { name: 'Team' }).click();
  await page.getByRole('option', { name: 'Quality Engineering', exact: true }).click();
  await expect(page.getByRole('combobox', { name: 'Team' })).toContainText('Quality Engineering');
});

Run the example with npx playwright test. For framework setup, review the Playwright TypeScript framework guide, then strengthen locator choices with Playwright locator best practices. Teams troubleshooting intermittent runs can also use the Playwright flaky test guide.

Interview Questions and Answers

Q: What is the first principle you would explain for native selects and custom dropdown widgets?

I would define an observable contract before selecting an API. The action alone is not proof. I would assert the state a user or downstream system depends on, using a locator or response that Playwright can retry safely.

Q: Why should a test avoid waitForTimeout()?

A fixed delay guesses how long the application needs. It wastes time when the UI is fast and still fails when CI is slower. I wait for a precise event, response, locator state, or polled condition with a meaningful timeout.

Q: How do you prevent races?

I create the promise for a relevant event or response before performing the action that can emit it. Then I await the action and the promise in a clear order. This prevents the application from completing before the listener exists.

Q: Which locators are most maintainable?

I prefer roles, accessible names, labels, and stable test IDs. These represent user-facing contracts and are less coupled to DOM structure. I use CSS only where it expresses a stable product contract.

Q: What belongs in a failure report?

The report should show the expected contract, actual state, relevant URL, and diagnostic artifacts. A retained trace is often more useful than a screenshot alone because it includes actions, DOM snapshots, console messages, and network activity.

Q: How would you make this coverage CI-safe?

I isolate data, bound waits and loops, avoid order dependence, and run against a controlled environment. I retain artifacts on failure and validate the same test under configured browser projects without adding browser-specific sleeps.

Common Mistakes

  • Waiting a fixed number of milliseconds instead of waiting for evidence.
  • Using brittle selectors tied to layout or generated classes.
  • Checking only that an action succeeded, without asserting business state.
  • Creating an event or response wait after the triggering action.
  • Allowing unbounded loops that can hang a worker.
  • Sharing mutable state between parallel tests.
  • Hiding a product defect with retries or broad exception handling.
  • Capturing secrets or customer data in traces and screenshots.

Treat each mistake as a review prompt. A stable suite is not one that never fails. It is one whose failures are reproducible, correctly classified, and rich enough to diagnose. Review helper abstractions periodically because a helper that hides event ownership or swallows assertions makes failures harder to understand.

Conclusion

The practical answer to playwright how to test a dropdown is to use Playwright's supported APIs, synchronize on observable outcomes, and assert the product contract with retrying locators. The exact command matters, but event ordering, isolation, and a meaningful final assertion matter more.

Start with one representative happy-path test, add a negative or boundary case, and run both repeatedly in the same CI configuration used by the team. Once those tests are trustworthy, extract only the repeated mechanics into a small helper or fixture.

Interview Questions and Answers

How would you design this test?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you avoid flaky synchronization?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

Which locator strategy would you choose?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you handle negative paths?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

What diagnostics do you retain?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

How do you keep the test maintainable?

I define the observable contract, perform the smallest user-level action, and assert the resulting state with a retrying locator. I keep waits bounded and attach them before their trigger. I also isolate test data and retain traces on failure so CI failures are actionable.

Frequently Asked Questions

What is the recommended starting point for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Should I use a fixed timeout for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

How should I choose locators for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

How do I debug a CI-only failure for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Should I create a reusable helper for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

What should the final assertion prove for native selects and custom dropdown widgets?

Start from the user-visible contract and use a supported Playwright API with a web-first assertion. Avoid fixed sleeps, keep synchronization explicit, and retain a trace when the failure needs more context.

Related Guides