feat: extend Cypress.Keyboard.Keys and cy.press to support (almost) all keyboard keys#31496
Conversation
There was a problem hiding this comment.
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 const CDP_KEYCODE: KeyCodeLookup = { | ||
| 'Tab': 'U+000009', | ||
| return KEY_HEX_TABLE[key as KeyPressSupportedKeys] !== undefined |
There was a problem hiding this comment.
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.
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.
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>;
};|
|
||
| export * from './preferences' | ||
|
|
||
| export { KeyPressSupportedKeys } from './automation' |
There was a problem hiding this comment.
| export { KeyPressSupportedKeys } from './automation' | |
| export type { KeyPressSupportedKeys } from './automation' |
There was a problem hiding this comment.
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.
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.
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.
It really doesn't matter here.
|
|
||
| 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.
Is the | string redundant given SupportedKey includes it?
There was a problem hiding this comment.
No, because SupportedKey always resolves to never unless the value is cast with toSupportedKey.
|
|
||
| /** | ||
| * 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.
| * 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.
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
There was a problem hiding this comment.
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.
| 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.
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.
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.
Can the map and reduce be combined to improve performance?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
|
|
||
| **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.
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.
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.
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.
i'd prefer the duplication for readability but maybe just me
jennifer-shehane
left a comment
There was a problem hiding this comment.
@cacieprins It would be nice to have a docs link here to on.cypress.io/press
jennifer-shehane
left a comment
There was a problem hiding this comment.
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.
@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?