-
Notifications
You must be signed in to change notification settings - Fork 3.3k
feat: extend Cypress.Keyboard.Keys and cy.press to support (almost) all keyboard keys #31496
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
packages/server/lib/automation/commands/key_press.ts:134
- The code mapping for 'ArrowUp' uses 'U+0010', which is also used for 'Shift' in this mapping. Please verify against the CDP specification and assign a unique key code for 'ArrowUp'.
'ArrowUp': 'U+0010',
cypress
|
Project |
cypress
|
Branch Review |
feat-all-keys
|
Run status |
|
Run duration | 12m 55s |
Commit |
|
Committer | Cacie Prins |
View all properties for this run ↗︎ |
Test results | |
---|---|
|
0
|
|
0
|
|
694
|
|
0
|
|
130
|
View all changes introduced in this branch ↗︎ |
it seems like we aren't normalizing |
We have this functionality mapped out! #31052 |
export function isSupportedKey (key: string): key is KeyPressSupportedKeys { | ||
return CDP_KEYCODE[key] && BIDI_VALUE[key] | ||
} | ||
|
||
export const CDP_KEYCODE: KeyCodeLookup = { | ||
'Tab': 'U+000009', | ||
return KEY_HEX_TABLE[key as KeyPressSupportedKeys] !== undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason not to narrow the parameter type to KeyPressSupportedKeys
? That change would remove the need for the type assertion inside the function.
export function isSupportedKey (key: KeyPressSupportedKeys): key is KeyPressSupportedKeys {
return typeof KEY_HEX_TABLE[key] !== 'undefined'
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've since changed how this works, but-- this is a type guard function, which casts the parameter of type T to the predicate type U.
function isCat (creature: Animal): creature is Cat {
return creature.family === 'Felidae`
}
For the function isCat
to be useful, its argument (Animal
) must be a supertype of the guarded type (Cat
). Otherwise you already know it's a Cat
, so why invoke the function? :)
for (const [key, code] of Object.entries(CDP_KEYCODE)) { | ||
it(`dispatches a keydown followed by a keyup event to the provided send fn with the ${key} keycode`, async () => { | ||
await cdpKeyPress({ key: key as KeyPressSupportedKeys }, sendFn, executionContexts, frameTree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might benefit from adding utils that return typed keys, values, and entries for objects. With these utils, you shouldn't need to use type assertions when working with Object.keys
, Object.values
, or Object.entries
.
I've added these utils in other projects, and they've improved the TypeScript experience while also helping catch potential issues.
type ObjectKey<T> = keyof T;
type ObjectKeys<T> = ObjectKey<T>[];
type ObjectValue<T> = T[ObjectKey<T>];
type ObjectValues<T> = ObjectValue<T>[];
type ObjectEntries<T> = [ObjectKey<T>, ObjectValue<T>][];
export const getTypedObjectKeys = <T extends object>(obj: T): ObjectKeys<T> => {
return Object.keys(obj) as ObjectKeys<T>;
};
export const getTypedObjectValues = <T extends object>(obj: T): ObjectValues<T> => {
return Object.values(obj) as ObjectValues<T>;
};
export const getTypedObjectEntries = <T extends object>(obj: T): ObjectEntries<T> => {
return Object.entries(obj) as ObjectEntries<T>;
};
packages/types/src/index.ts
Outdated
@@ -4,6 +4,8 @@ export * from './constants' | |||
|
|||
export * from './preferences' | |||
|
|||
export { KeyPressSupportedKeys } from './automation' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export { KeyPressSupportedKeys } from './automation' | |
export type { KeyPressSupportedKeys } from './automation' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has since been updated to export * from './automation'
, obviating the need to declare it as a type export.
for (const [key, value] of Object.entries(BIDI_VALUE)) { | ||
// special formatting for value; otherwise test output will show the rendered unicode character | ||
it(`dispatches a keydown and keyup action with the value '\\u${(value as string).charCodeAt(0).toString(16).toUpperCase()}' for key '${key}'`, async () => { | ||
await bidiKeyPress({ key: key as KeyPressSupportedKeys }, client as WebdriverClient, autContext, 'idSuffix') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is the WebdriverClient
type assertion necessary? client
appears to satisfy the type constraint of the second parameter in the bidiKeyPress
function without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was at a previous point to this, but is no longer. Thanks, it's been removed!
Converting to draft for now - there are major cross-browser issues with using the unicode codepoints as they are; this implementation (not the public signature) needs to be rethought. |
…, support any single character even if multiple codepoints
…assert in that special case
cy.visit('/fixtures/input_events.html') | ||
}) | ||
|
||
const testKeyDownUp = (key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should key
be typed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It really doesn't matter here.
import $errUtils from '../../../cypress/error_utils' | ||
import $utils from '../../../cypress/utils' | ||
|
||
export interface PressCommand { | ||
(key: KeyPressSupportedKeys, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>): void | ||
(key: SupportedKey | string, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the | string
redundant given SupportedKey
includes it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because SupportedKey
always resolves to never
unless the value is cast with toSupportedKey
.
packages/types/src/automation.ts
Outdated
|
||
/** | ||
* Union type representing all keys supported by cy.press(). | ||
* Includes single-character strings (inluding unicode characters with multiple code points) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Includes single-character strings (inluding unicode characters with multiple code points) | |
* Includes single-character strings (including unicode characters with multiple code points) |
* Must be cast to via `toSupportedKey` or guarded with `isSupportedKey` | ||
* to ensure it is a valid key. | ||
*/ | ||
export type SupportedKey = SupportedKeyType & string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason the type of SupportedKey
here doesn't match the type in cypress-automation.d.ts
?
cypress/cli/types/cypress-automation.d.ts
Line 28 in f63b212
type SupportedKey = SupportedNamedKey | string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's so users won't have to .toSupportedKey()
whatever value they pass in.
@@ -149,38 +151,94 @@ describe('key:press automation command', () => { | |||
}) | |||
|
|||
it('dispaches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('dispaches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => { | |
it('dispatches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => { |
}) | ||
|
||
it('dispatches one keydown followed by a keyup event for each codepoint', async () => { | ||
await bidiKeyPress(key, client, autContext, 'idSuffix') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve noticed that the fourth parameter in all bidiKeyPress
calls in this file is the same. Should there be some variation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd prefer the duplication for readability but maybe just me
.map((value): InputKeySourceAction[] => { | ||
return [ | ||
{ type: 'keyDown', value }, | ||
{ type: 'keyUp', value }, | ||
] | ||
}) | ||
.reduce((arr, el) => [...arr, ...el], []), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the map
and reduce
be combined to improve performance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The longest the value can be, for the sake of single-pair combination utf-8 characters, is 2. Legibility is more important than performance when n<=2.
const activeWindow = await getActiveWindow(client) | ||
const { contexts: [{ context: topLevelContext }] } = await client.browsingContextGetTree({}) | ||
|
||
const key: string = BidiOverrideCodepoints[inKey] ?? inKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to suggest removing the string
type, since it should be inferred. However, I noticed that the type of inKey
is being resolved as never
. Could that be an issue?
I also see that variables typed as SupportedKey
in packages/server/lib/automation/commands/key_press.ts
are being resolved to never
as well. I’m wondering if any type errors would appear if these variables were not resolving to never
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SupportedKey
is a bit of a weird type here. It needs to include:
- Named Keys
- Single-codepoint utf-8 characters
- Multi-codepoint utf-8 characters
but exclude:
- Multiple characters
Given TS does not have a "character" type, this is a bit of a hack to ensure that the value of the string is programmatically ensured to be either a named key or a single character (allowing for multiple utf-8 codepoints).
Unless toSupportedKey
is called (to ensure the check), SupportedKey
will always resolve to never
.
// multi-codepoint characters must be dispatched as individual codepoints | ||
const isNamedKey = NamedKeys.includes(key) | ||
|
||
const chars = [...key].length === 1 || isNamedKey ? [key] : [...key] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this code be simplified to improve performance?
const chars = [...key].length === 1 || isNamedKey ? [key] : [...key] | |
const chars = isNamedKey ? [key] : [...key]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt there would be any measurable performance here. The suggestion also doesn't have the same effect
{ type: 'keyDown', value }, | ||
{ type: 'keyUp', value }, | ||
], | ||
id: `${autContext}-${inKey}-${idSuffix || Date.now()}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logical OR encourages the use of ''
as a suffix. Also, will Date.now()
collide if multiple calls are made in the same millisecond?
id: `${autContext}-${inKey}-${idSuffix || Date.now()}`, | |
id: `${autContext}-${inKey}-${idSuffix ?? crypto.randomUUID()}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logical OR results in there always being a suffix, even if ''
is passed in - an empty string is falsey. A nullish coalescing operator will result in context-key-
, which is not quite what we want here.
TBH, the BiDi spec is very vague on what this id
needs to be, and given the nature of how Cypress' server works, it's very unlikely to collide. I'm okay with switching it over to a random uuid. However, Date.now()
is what was there previously, so that kind of refactor (including test updates) should probably be covered in a separate PR.
cli/CHANGELOG.md
Outdated
@@ -3,6 +3,10 @@ | |||
|
|||
_Released 08/26/2025 (PENDING)_ | |||
|
|||
**Features:** | |||
|
|||
- Expanded `cy.press()` to support more key types. Addresses [#31051](https://github.com/cypress-io/cypress/issues/31051). Addressed in [#31496](https://github.com/cypress-io/cypress/pull/31496). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
likely need to bump the version to 15.1.0
in the changelog. I can do that in the release PR if needed
return | ||
} | ||
// Non-BiDi firefox is not supported | ||
if (Cypress.browser.family === 'firefox' && Cypress.browserMajorVersion() < 135) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think now that we are on 15 Cypress fails before this because we support bidi only? Likely don't need this check
// multi-codepoint characters must be dispatched as individual codepoints | ||
const isNamedKey = NamedKeys.includes(key) | ||
|
||
const chars = [...key].length === 1 || isNamedKey ? [key] : [...key] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt there would be any measurable performance here. The suggestion also doesn't have the same effect
}) | ||
|
||
it('dispatches one keydown followed by a keyup event for each codepoint', async () => { | ||
await bidiKeyPress(key, client, autContext, 'idSuffix') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'd prefer the duplication for readability but maybe just me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cacieprins It would be nice to have a docs link here to on.cypress.io/press

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This actually works, the output is a bit weird though since it prints nothing. Not saying this has to be addressed, just commenting.
// Test tab character
cy.press('\t')
cy.get('#lastKey').should('contain', 'Tab')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cacieprins Oh, this seems like it should be handled better
cy.press(123)

This also happens on the below, which I think SHOULD work.
cy.press(0)
Released in This comment thread has been locked. If you are still experiencing this issue after upgrading to |
Additional details
Include additional key support to cy.press. See https://github.com/cypress-io/cypress-documentation/blob/release/15.1.0/docs/api/commands/press.mdx for full list of keys
Steps to test
How has the user experience changed?
PR Tasks
cypress-documentation
? docs: updates cy.press() documentation to include expanded named keys, as well as utf-8 characters cypress-documentation#6256type definitions
?