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+Enterto submit a composerMeta+Kto open a command paletteShift+Tabto move focus backwardsControl+Abefore 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+Keyform in one place: split on the last+, mapControl/Ctrl,Shift,Alt,Meta/Cmd/Commandto the correspondingctrlKey/shiftKey/altKey/metaKey, and accept several (Control+Shift+Z). The remainder is the key. - Dispatch the modifier keys' own
keydownbefore, andkeyupafter, as a browser does: pressingControl+afireskeydown{Control},keydown{a},keyup{a},keyup{Control}. - Populate
codefrom the key —KeyA,Digit1,Enter,Tab,ArrowDown,ShiftLeft. A table is needed, and a single-character key maps toKey<UPPER>/Digit<n>mechanically. keyboard.down(key)andkeyboard.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 primitivepress()is built from.locator.press(key)(#1608) should route through the same parsing rather than grow its own.
Acceptance
press('Control+a')fireskeydownwithkey: 'a'andctrlKey: true.- The modifier's own keydown/keyup surround the key's, asserted by event order.
press('Control+Shift+Z')sets both flags.event.codeisKeyAfora,EnterforEnter,Digit1for1.- A plain
press('Enter')is unchanged, asserted, so the parsing does not start splitting key names that merely contain a+. keyboard.down('Shift')thenpress('Tab')reportsshiftKey: true.
Found while sweeping the remaining Playwright gaps after #1607.