ReviewOS

stacks/very-happy-dom

keyboard.press('Control+a') fires key 'Control+a' with no modifier flags set

#1615
Closed glennmichael123 opened this 8 days ago ·
8 days ago

Problem

keyboard.press() treats a modifier combination as a single key name.

const page = new Browser().newPage()
page.mainFrame.window.document.body.innerHTML = '<input id="i">'
const input = page.mainFrame.window.document.querySelector('#i')
input.addEventListener('keydown', e => console.log({ key: e.key, ctrl: e.ctrlKey }))

await page.locator('#i').focus()
await page.keyboard.press('Control+a')
// { key: 'Control+a', ctrl: false }      expected { key: 'a', ctrl: true }

await page.keyboard.press('Shift+Tab')
// { key: 'Shift+Tab', shift: false }     expected { key: 'Tab', shift: true }

A plain key is fine — press('Enter') gives key: 'Enter'. It is only the Modifier+Key form, which is the documented way to express one in Playwright and Puppeteer alike, that is passed through unparsed.

event.code is also always '', for every key.

Why it matters

The handler never runs, and nothing reports a problem. press() resolves, a keydown fires, and a listener checking event.key === 'a' && event.ctrlKey simply does not match — so the test asserts that nothing happened and passes, because nothing did.

The affected shortcuts are the ones applications actually bind:

  • Control+Enter / Meta+Enter to submit a composer
  • Meta+K to open a command palette
  • Shift+Tab to move focus backwards
  • Control+A before typing, to replace a field's contents

key: 'Control+a' is not a value any real KeyboardEvent can carry, so there is no browser in which a handler would see it. Code branching on event.key in a switch falls through to the default case.

event.code being empty matters for the same class of handler: code is what you read when you want the physical key regardless of layout, so event.code === 'KeyA' never matches either.

Proposed scope

  • Parse the Modifier+Key form in one place: split on the last +, map Control/Ctrl, Shift, Alt, Meta/Cmd/Command to the corresponding ctrlKey / shiftKey / altKey / metaKey, and accept several (Control+Shift+Z). The remainder is the key.
  • Dispatch the modifier keys' own keydown before, and keyup after, as a browser does: pressing Control+a fires keydown{Control}, keydown{a}, keyup{a}, keyup{Control}.
  • Populate code from the key — KeyA, Digit1, Enter, Tab, ArrowDown, ShiftLeft. A table is needed, and a single-character key maps to Key<UPPER> / Digit<n> mechanically.
  • keyboard.down(key) and keyboard.up(key) are missing and belong with this: they are how a test holds a modifier across several keys, and once modifier state is tracked they are the primitive press() is built from.
  • locator.press(key) (#1608) should route through the same parsing rather than grow its own.

Acceptance

  • press('Control+a') fires keydown with key: 'a' and ctrlKey: true.
  • The modifier's own keydown/keyup surround the key's, asserted by event order.
  • press('Control+Shift+Z') sets both flags.
  • event.code is KeyA for a, Enter for Enter, Digit1 for 1.
  • A plain press('Enter') is unchanged, asserted, so the parsing does not start splitting key names that merely contain a +.
  • keyboard.down('Shift') then press('Tab') reports shiftKey: true.

Found while sweeping the remaining Playwright gaps after #1607.

Sign in to comment on this issue.