VALIDATION LAB•SEARCHABLE SELECT
Selection
Searchable Select
Type-to-filter a country list, navigate suggestions with the keyboard, and pick by Enter.
Scenario
Country / currency / user pickers in admin UIs all share the same shape. Bugs hide in: case-insensitive match, accent-folding (Türkiye vs Turkiye), keyboard nav, click-outside-to-close, and Enter on an empty filter.
Selected: —
Live widget · interact freely
Manual test checklist
- 1Type 'tu' (lowercase) — confirm 'Turkey' shows up (case-insensitive)
- 2Type 'tür' with diacritic — should still match 'Turkey'
- 3Use ↑/↓ to highlight, then Enter — selection should match the highlight
- 4Click outside the menu — popup closes without selecting
- 5Open with no filter, press Enter — must NOT select anything
- 6Empty result state shows when the filter returns zero matches
Expected result
Typing 'tu' narrows the list to Turkey + Tunisia; ↓↓ Enter selects the second option and closes the popup.
Automation challenge
Type 'tu' into `country-input`, assert at least one suggestion is visible, then drive the keyboard: `keyboard.press('ArrowDown')` × 2 and `Enter`. Assert the selected chip text matches the highlighted item.
Stable selectors
- Country input
[data-testid="country-input"] - Suggestion list
[data-testid="country-list"] - Suggestion row template
[data-testid="country-option-{id}"] - Selected chip
[data-testid="country-selected"] - Empty state
[data-testid="country-empty"]
Locator strategy
Three levels from simple IDs to scoped Playwright locators. IDs and names are easy to learn but are not always the best long-term choice when labels change or components repeat.
Use simple IDs and names to understand how locating elements works.
123
await page.goto('https://lab.hakdogan.com/practice/searchable-select');
await page.locator('#country-input').fill('tu');Avoid as primary strategies
XPath (unless there is no alternative), long CSS chains, Tailwind-style utility class selectors, generated or unstable IDs, and volatile framework internals break when layout, styling, or DOM structure shifts.
12
await page.locator('.w-full.rounded-xl.border.bg-blue-500').fill('demo@example.com');
await page.locator('//div[2]/form/div[1]/input').fill('demo@example.com');Reference Playwright spec
1234567891011121314151617
import { test, expect } from '@playwright/test';
test('searchable select picks by keyboard', async ({ page }) => {
await page.goto('https://lab.hakdogan.com/practice/searchable-select');
await page.getByTestId('country-input').fill('tu');
await expect(page.getByTestId('country-list')).toBeVisible();
await expect(
page.locator('[data-testid^="country-option-"]'),
).toHaveCount(2);
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.getByTestId('country-selected')).toContainText('Tunisia');
});Headed Test Playback
Simulated headed-browser flow — no real browser is launched.
Automation-style playback (Playwright-shaped logs). No real browser; no commands run on your machine.
searchable-select.spec.ts- Interacting with searchable select widget
- Awaiting deterministic render
- Asserting expected state
- Cleaning up context
- Pass