skip to content

In Playwright, why might a comment box's @mention autocomplete never open when the test uses locator.fill()?

level: seniorimportance: should knowfreq 41%

answer

  1. The value is right, the widget is missing
  2. Look at which step actually failed
  3. One insertion, no key events
  4. Handlers listening for keydown never ran
  5. Type only the trigger, fill the rest

basics

~20 s

fill inserts the whole string in one step and dispatches only an input event, never keydown or keyup. A menu that opens on key handlers therefore never sees a trigger. Type the trigger characters with pressSequentially instead.

solid answer

~40 s

The value lands but the widget never reacts, because `locator.fill()` inserts the string in a single step: the page sees one `input` event and no key events. A mention menu keyed off `keydown` or `keyup` has nothing to react to. The tell in the trace is a correct field value with no menu in the DOM, and a failure reported on the *click* for the menu item rather than on the fill. The fix is scoped, not global: `fill` the prose, then `pressSequentially('@dan')` for the characters that must arrive as keys, adding `{ delay: 50 }` if the lookup is debounced. Because the field is already focused, typing continues at the caret. The same failure family covers input masks, search-as-you-type and keystroke counters — all key-driven, none of them reachable by `fill`.

code

typescript · 12 lines
typescript
import { test, expect } from '@playwright/test';

test('mention a teammate in a comment', async ({ page }) => {
  await page.goto('https://tracker.example.com/issues/DEV-114');
  const composer = page.getByRole('textbox', { name: 'Add a comment' });

  await composer.fill('Reassigning to ');        // bulk prose, one insertion
  await composer.pressSequentially('@dan');      // key events open the menu

  await page.getByRole('option', { name: 'dana.k' }).click();
  await expect(composer).toHaveValue('Reassigning to @dana.k ');
});

go deeper

for a junior

Learn the tell: if a field holds the right text but a widget that should react never appears, the missing ingredient is key events rather than the value itself.

for a middle

Explain that fill produces one input event and no keydown or keyup, and that pressSequentially restores the per-character key sequence a menu listens for.

for a senior

Diagnose from the artefacts — which step failed, what the trace snapshot shows — and fix narrowly, typing only the trigger characters instead of converting the suite.

for a principal

Decide where key-sensitive widgets are covered at all. An end-to-end test proves the integration once; the keystroke edge cases belong in a faster layer close to the component.

## The symptom and the mechanism A comment composer on an issue tracker opens a mention menu when the user types `@`. A Playwright test does `composer.fill('Reassigning to @dana')`, the value lands correctly, and the menu never appears — so the click on `dana.k` times out and the test fails on the *next* line, which is why this one is usually misdiagnosed. The cause is what `fill` dispatches. `fill` focuses the field, selects any existing text and inserts the whole string in one step; the page observes a single `input` event and **no key events at all**. A mention menu is almost always wired to `keydown` or `keyup` so it can inspect the key that was pressed and the caret position. Those handlers never run, so the menu never opens. Note what the problem is *not*. `fill` goes through the browser's real input pipeline, so the field's value is genuinely set and a React or Vue binding updates normally. Anything reading `event.target.value` on `input` sees the text. Only key-level listeners are starved. ## Diagnosing it, not guessing 1. **Check the failing step.** The failure is on the click for the menu item, not on the fill, so read the call log for the *click*: it will say it was waiting for an element that never resolved. 2. **Look at the trace's DOM snapshot after the fill.** The field holds the right text and no menu is in the DOM. That combination — correct value, absent widget — is the signature of a key-driven feature that never fired. 3. **Confirm in the page's own code or in the browser.** Typing `@` by hand opens the menu; setting the same value from the console does not. If both fail, the bug is in the product, not the test. ## The fix, scoped Do not swap every `fill` in the suite for typing. Fill the bulk of the text, then type only the characters that must arrive as keys: - `composer.fill('Reassigning to ')` — one step for the prose nobody's handler cares about. - `composer.pressSequentially('@dan')` — real `keydown`, `keypress`/`input` and `keyup` per character, which is what opens the menu. - `page.getByRole('option', { name: 'dana.k' }).click()` — pick from the menu that is now open. Because the field is already focused after the fill, the typed characters continue at the caret rather than replacing anything, which is precisely what you want here. If the menu debounces its lookup, add `{ delay: 50 }` so a request has room between characters. ## The other members of this failure family | Symptom | Why fill fails | What to do | |---|---|---| | Mention or slash-command menu never opens | opens on `keydown` | type the trigger characters | | Input mask formats nothing | rewrites the value per keystroke | type the whole value | | Search-as-you-type never fires | debounced on `keyup` | type, with a `delay` | | Keystroke counter stays at zero | counts key events | type, or test that at the unit level | | Field rejects the value entirely | non-fillable input type | use the right verb for the control | ## What this costs and when to stop Typing is one protocol round trip per character. A twenty-character mention handle is twenty steps, each with a window in which a re-render can move focus. That is affordable for three or four characters and expensive as a suite-wide default, so keep the switch as narrow as the mechanism demands: type the trigger, fill everything else. If a widget is so key-sensitive that its behaviour needs dozens of realistic keystrokes to exercise, that is a signal the coverage belongs closer to the component than to the end-to-end suite. Use the end-to-end test to prove the mention menu integrates — one handle, one selection, one saved comment — and leave the key-handling edge cases to a faster layer.

  • How do you tell this apart from a genuine product bug in the mention menu?
    Reproduce by hand. If typing `@` in the browser opens the menu but the test's fill does not, the gap is key events and the test is wrong. If typing by hand also fails, the product is broken. The trace's DOM snapshot after the fill — right value, no menu — is the first clue.
  • Why not just replace every fill in the suite with pressSequentially?
    Because typing is one round trip per character. A forty-character description becomes forty steps, each a window for a re-render to steal focus, and none of it buys coverage on fields that only read `value`. Keep the switch to the field whose mechanism actually needs keys.
  • The mention lookup is debounced and still misses characters. What next?
    Pass `{ delay: 50 }` to `pressSequentially` so each keystroke has room for the debounced request, and assert on the menu with a web-first matcher rather than on a fixed wait. If it stays flaky, the widget's key handling is better covered a layer down than end to end.

saying these in an interview costs you the question

  • fill does not update the value, so nothing downstream works
  • The framework binding is broken by fill
  • Add a fixed wait after the fill and the menu appears
  • Switch the whole suite to typing to be safe
  • The fill step is the one that failed
  • Only a real browser click can open a mention menu