VALIDATION LABSEARCHABLE SELECT

Selection

Searchable Select

Type-to-filter a country list, navigate suggestions with the keyboard, and pick by Enter.

Intermediate

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.

1
2
3
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.

1
2
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

searchable-select.spec.ts
ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

Idlesearchable-select.spec.ts
Failure demos
https://lab.hakdogan.com/practice/searchable-select
playwright · headed · chromium0.00s
# awaiting Run Test · terminal scrolls automatically
Steps
  1. Interacting with searchable select widget
  2. Awaiting deterministic render
  3. Asserting expected state
  4. Cleaning up context
  5. Pass
StatusIdle
BrowserChromium
FrameworkPlaywright + TypeScript
Elapsed (live)--
Specsearchable-select.spec.ts