Skip to content

feat: fillField works with role=combobox widgets - #5701

Closed
DavertMik wants to merge 1 commit into
4.xfrom
fix/fill-field-combobox
Closed

feat: fillField works with role=combobox widgets#5701
DavertMik wants to merge 1 commit into
4.xfrom
fix/fill-field-combobox

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Problem

I.fillField('Country', 'Ukr') does not work against comboboxes whose trigger is not a text field — the shadcn / Base UI pattern where a <button role="combobox"> opens a popup containing the search input.

Worse, against Base UI it silently passes while doing nothing. Base UI renders:

<label for="value-input" id="country-label">Country</label>
<button role="combobox" aria-labelledby="country-label"></button>
<input id="value-input" name="content" tabindex="-1" aria-hidden="true" style="clip:rect(0,0,0,0);position:fixed">

The <label for> points at the aria-hidden form-value proxy, not at the trigger. Locator.field.labelEquals is tried first in findFields and matches that proxy, so fillField types into a zero-size invisible input and the step resolves green while the app never sees the interaction.

What changed

Locator.field skips aria-hidden proxies (lib/locator.js) — one predicate added to the input | textarea | select branches of all four builders, so all three helpers are fixed in one place. With the proxy filtered, resolution falls through to labelContainsaria-labelledby → the trigger.

fillField handles combobox triggers (lib/helper/extras/comboBox.js) — a <button role="combobox">, a <div role="combobox">, or a readonly input trigger is expanded, its search input located, and the value typed there. The popup is left open and filtered so an option can be picked:

I.fillField('Country', 'Ukr')
I.click('Ukraine')

Locating the input walks, in order: an editable descendant of the trigger (ARIA 1.1 wrapper, no click needed), the aria-controls/aria-owns container, a visible [role=dialog]/[role=listbox] popup, then the focused element. Two details are measured rather than assumed:

  • Focus lands asynchronously ~80ms after click() resolves in Base UI (pointerdown t=0 → click t=3 → focusin t=82), so the search is polled to a 1s deadline instead of sampled once.
  • The element focused before the click is excluded, so a previously focused field never receives the keystrokes.

A combobox exposing no text input raises combobox exposes no text input to type into. Use I.selectOption() to pick one of its options. rather than typing into nothing.

role="combobox" inputs (Base UI inline, MUI, Downshift) also route through this path, so they are typed with real keystrokes rather than a one-shot fill() — comboboxes that filter on keydown need that.

Detection reuses the single evaluate that richTextEditor already runs per fillField, so no extra round-trip is added to ordinary fills.

Tests

New belt in test/helper/webapi.js, so it runs against Playwright, Puppeteer and WebDriver — mirroring the rich-editor belt. Fixtures load Base UI 1.0.0-rc.0 from CDN with no build step (test/data/app/view/form/combobox/), covering the button trigger, the inline input, and custom_select.php as the negative case.

Each variant asserts: typing filters the options, the picked option submits, a pre-populated value is rewritten, and keystrokes do not leak to an outer focused input. Locator.field gets unit tests in test/unit/locator_test.js.

Local: 9/9 combobox belt on Playwright and Puppeteer, 75/75 fillField on Playwright (rich editors included, no regressions), 15/15 selectOption, 772 unit tests. WebDriver has no local server here — relying on CI.

Note for review: test/data/app/view/form/combobox/* pull React and Base UI from esm.sh, a CDN this repo has not used before (existing fixtures use jsdelivr). Versions are pinned exactly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ut1rjeUiZmKU7V61CeYhns

fillField now handles comboboxes whose trigger is not a text field —
a `<button role="combobox">` (shadcn, Base UI), a `<div role="combobox">`,
or a readonly input trigger. The widget is expanded, its search input is
located, and the value is typed there, leaving the list filtered so an
option can be picked with a following click.

Locating the input walks: an editable descendant of the trigger, the
`aria-controls`/`aria-owns` container, a visible dialog/listbox popup,
then the focused element. Focus lands asynchronously after the click
(~80ms in Base UI), so the search is polled to a 1s deadline rather than
sampled once. The element focused before the click is excluded, so a
previously focused field never receives the keystrokes. A combobox with
no text input raises an error pointing at selectOption.

Also fixes Locator.field matching an aria-hidden proxy input. Base UI
points `<label for>` at a visually hidden form-value input rather than
at the trigger, so fillField typed into an invisible field and passed
while the app saw nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ut1rjeUiZmKU7V61CeYhns
@DavertMik

Copy link
Copy Markdown
Contributor Author

Absoltely terrible code

@DavertMik DavertMik closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant