Skip to content

Commit cfdeb7a

Browse files
feat: cy.press() (#31398)
* wip - cy.press() command * cy command for dispatching key press/down/up events * unit tests and failure cases for cy.press() * Cypress.Keyboard.Keys definition; fix command log message * add keys to the internal keyboard type * auto-focus in cdp * ensure aut iframe is focused before dispatching key events in bidi browsers * update tests for cdp focus * fixed tests for bidi * lint * fix type ref in .d.ts * linting * skip press() driver test in ff below v135 * try all contexts for frame before failing due to missing/invalid context id * ensure error is error before accessing props * skip press driver test in webkit * changelog * debug automation middleware invocation for firefox flake * debug * cache update * use bidi automation middleware from connectToNewSpec rather than constructor * more comprehensive logging * debug socket base, additional debug in automation * install firefox automation middleware on setup as well as connectToNewSpec * unit tests for firefox-utils * proper calledWith --------- Co-authored-by: Jennifer Shehane <[email protected]>
1 parent 9155e05 commit cfdeb7a

File tree

27 files changed

+791
-152
lines changed

27 files changed

+791
-152
lines changed

cli/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
2-
## 14.2.2
2+
## 14.3.0
33

44
_Released 4/8/2025 (PENDING)_
55

6+
**Features:**
7+
8+
- The [`cy.press()`](https://on.cypress.io/api/press) command is now available. It supports dispatching native Tab keyboard events to the browser. Addresses [#31050](https://github.com/cypress-io/cypress/issues/31050). Addresses [#299](https://github.com/cypress-io/cypress/issues/299). Addressed in [#31398](https://github.com/cypress-io/cypress/pull/31398).
9+
610
**Bugfixes:**
711

812
- Allows for `babel-loader` version 10 to be a peer dependency of `@cypress/webpack-preprocessor`. Fixed in [#31218](https://github.com/cypress-io/cypress/pull/31218).

cli/types/cypress.d.ts

Lines changed: 108 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -578,99 +578,7 @@ declare namespace Cypress {
578578
*/
579579
stop(): void
580580

581-
Commands: {
582-
/**
583-
* Add a custom command
584-
* @see https://on.cypress.io/api/commands
585-
*/
586-
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
587-
588-
/**
589-
* Add a custom parent command
590-
* @see https://on.cypress.io/api/commands#Parent-Commands
591-
*/
592-
add<T extends keyof Chainable>(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn<T>): void
593-
594-
/**
595-
* Add a custom child command
596-
* @see https://on.cypress.io/api/commands#Child-Commands
597-
*/
598-
add<T extends keyof Chainable, S = any>(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject<T, S>): void
599-
600-
/**
601-
* Add a custom child or dual command
602-
* @see https://on.cypress.io/api/commands#Validations
603-
*/
604-
add<T extends keyof Chainable, S extends PrevSubject>(
605-
name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
606-
): void
607-
608-
/**
609-
* Add a custom command that allows multiple types as the prevSubject
610-
* @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types
611-
*/
612-
add<T extends keyof Chainable, S extends PrevSubject>(
613-
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
614-
): void
615-
616-
/**
617-
* Add one or more custom commands
618-
* @see https://on.cypress.io/api/commands
619-
*/
620-
addAll<T extends keyof Chainable>(fns: CommandFns): void
621-
622-
/**
623-
* Add one or more custom parent commands
624-
* @see https://on.cypress.io/api/commands#Parent-Commands
625-
*/
626-
addAll<T extends keyof Chainable>(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void
627-
628-
/**
629-
* Add one or more custom child commands
630-
* @see https://on.cypress.io/api/commands#Child-Commands
631-
*/
632-
addAll<T extends keyof Chainable, S = any>(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject<S>): void
633-
634-
/**
635-
* Add one or more custom commands that validate their prevSubject
636-
* @see https://on.cypress.io/api/commands#Validations
637-
*/
638-
addAll<T extends keyof Chainable, S extends PrevSubject>(
639-
options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
640-
): void
641-
642-
/**
643-
* Add one or more custom commands that allow multiple types as their prevSubject
644-
* @see https://on.cypress.io/api/commands#Allow-Multiple-Types
645-
*/
646-
addAll<T extends keyof Chainable, S extends PrevSubject>(
647-
options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject<PrevSubjectMap<void>[S]>,
648-
): void
649-
650-
/**
651-
* Overwrite an existing Cypress command with a new implementation
652-
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
653-
*/
654-
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
655-
656-
/**
657-
* Overwrite an existing Cypress command with a new implementation
658-
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
659-
*/
660-
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
661-
662-
/**
663-
* Add a custom query
664-
* @see https://on.cypress.io/api/custom-queries
665-
*/
666-
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void
667-
668-
/**
669-
* Overwrite an existing Cypress query with a new implementation
670-
* @see https://on.cypress.io/api/custom-queries
671-
*/
672-
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): void
673-
}
581+
Commands: Commands
674582

675583
/**
676584
* @see https://on.cypress.io/cookies
@@ -775,6 +683,9 @@ declare namespace Cypress {
775683
*/
776684
Keyboard: {
777685
defaults(options: Partial<KeyboardDefaultsOptions>): void
686+
Keys: {
687+
TAB: 'Tab',
688+
},
778689
}
779690

780691
/**
@@ -829,6 +740,100 @@ declare namespace Cypress {
829740
onSpecWindow: (window: Window, specList: string[] | Array<() => Promise<void>>) => void
830741
}
831742

743+
interface Commands {
744+
/**
745+
* Add a custom command
746+
* @see https://on.cypress.io/api/commands
747+
*/
748+
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
749+
750+
/**
751+
* Add a custom parent command
752+
* @see https://on.cypress.io/api/commands#Parent-Commands
753+
*/
754+
add<T extends keyof Chainable>(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn<T>): void
755+
756+
/**
757+
* Add a custom child command
758+
* @see https://on.cypress.io/api/commands#Child-Commands
759+
*/
760+
add<T extends keyof Chainable, S = any>(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject<T, S>): void
761+
762+
/**
763+
* Add a custom child or dual command
764+
* @see https://on.cypress.io/api/commands#Validations
765+
*/
766+
add<T extends keyof Chainable, S extends PrevSubject>(
767+
name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
768+
): void
769+
770+
/**
771+
* Add a custom command that allows multiple types as the prevSubject
772+
* @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types
773+
*/
774+
add<T extends keyof Chainable, S extends PrevSubject>(
775+
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
776+
): void
777+
778+
/**
779+
* Add one or more custom commands
780+
* @see https://on.cypress.io/api/commands
781+
*/
782+
addAll<T extends keyof Chainable>(fns: CommandFns): void
783+
784+
/**
785+
* Add one or more custom parent commands
786+
* @see https://on.cypress.io/api/commands#Parent-Commands
787+
*/
788+
addAll<T extends keyof Chainable>(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void
789+
790+
/**
791+
* Add one or more custom child commands
792+
* @see https://on.cypress.io/api/commands#Child-Commands
793+
*/
794+
addAll<T extends keyof Chainable, S = any>(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject<S>): void
795+
796+
/**
797+
* Add one or more custom commands that validate their prevSubject
798+
* @see https://on.cypress.io/api/commands#Validations
799+
*/
800+
addAll<T extends keyof Chainable, S extends PrevSubject>(
801+
options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
802+
): void
803+
804+
/**
805+
* Add one or more custom commands that allow multiple types as their prevSubject
806+
* @see https://on.cypress.io/api/commands#Allow-Multiple-Types
807+
*/
808+
addAll<T extends keyof Chainable, S extends PrevSubject>(
809+
options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject<PrevSubjectMap<void>[S]>,
810+
): void
811+
812+
/**
813+
* Overwrite an existing Cypress command with a new implementation
814+
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
815+
*/
816+
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
817+
818+
/**
819+
* Overwrite an existing Cypress command with a new implementation
820+
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
821+
*/
822+
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
823+
824+
/**
825+
* Add a custom query
826+
* @see https://on.cypress.io/api/custom-queries
827+
*/
828+
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void
829+
830+
/**
831+
* Overwrite an existing Cypress query with a new implementation
832+
* @see https://on.cypress.io/api/custom-queries
833+
*/
834+
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): void
835+
}
836+
832837
type CanReturnChainable = void | Chainable | Promise<unknown>
833838
type ThenReturn<S, R> =
834839
R extends void ? Chainable<S> :
@@ -1742,6 +1747,16 @@ declare namespace Cypress {
17421747
*/
17431748
pause(options?: Partial<Loggable>): Chainable<Subject>
17441749

1750+
/**
1751+
* Send a native sequence of keyboard events: keydown & press, followed by keyup, for the provided key.
1752+
* Supported keys index the Cypress.Keyboard.Keys record.
1753+
*
1754+
* @example
1755+
* cy.press(Cypress.Keyboard.Keys.TAB) // dispatches a keydown and press event to the browser, followed by a keyup event.
1756+
* @see https://on.cypress.io/press
1757+
*/
1758+
press(key: typeof Cypress.Keyboard.Keys[keyof typeof Cypress.Keyboard.Keys], options?: Partial<Loggable & Timeoutable>): void
1759+
17451760
/**
17461761
* Get the immediately preceding sibling of each element in a set of the elements.
17471762
*

cli/types/tslint.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"jsdoc-format": false,
2525
// for now keep the Cypress NPM module API
2626
// in its own file for simplicity
27-
"no-single-declare-module": false
27+
"no-single-declare-module": false,
28+
// This is detecting necessary qualifiers as unnecessary
29+
"no-unnecessary-qualifier": false
2830
},
2931
"linterOptions": {
3032
"exclude": [
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
describe('__placeholder__/commands/actions/press', () => {
1+
describe('src/cy/commands/actions/press', () => {
22
it('dispatches the tab keypress to the AUT', () => {
3-
cy.visit('/fixtures/input_events.html')
3+
// Non-BiDi firefox is not supported
4+
if (Cypress.browser.family === 'firefox' && Cypress.browserMajorVersion() < 135) {
5+
return
6+
}
47

5-
cy.get('#focus').focus().then(async () => {
6-
try {
7-
await Cypress.automation('key:press', { key: 'Tab' })
8-
} catch (e) {
9-
if (e.message && (e.message as string).includes('key:press')) {
10-
cy.log(e.message)
8+
// TODO: Webkit is not supported. https://github.com/cypress-io/cypress/issues/31054
9+
if (Cypress.isBrowser('webkit')) {
10+
return
11+
}
1112

12-
return
13-
}
13+
cy.visit('/fixtures/input_events.html')
1414

15-
throw e
16-
}
15+
cy.press(Cypress.Keyboard.Keys.TAB)
1716

18-
cy.get('#keyup').should('have.value', 'Tab')
17+
cy.get('#keydown').should('have.value', 'Tab')
1918

20-
cy.get('#keydown').should('have.value', 'Tab')
21-
})
19+
cy.get('#keyup').should('have.value', 'Tab')
2220
})
2321
})

packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ it('verifies number of cy commands', () => {
223223
'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile',
224224
'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit',
225225
'focused', 'get', 'contains', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
226-
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
226+
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', 'press',
227227
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
228228
'mount', 'as', 'root', 'getAllLocalStorage', 'clearAllLocalStorage', 'getAllSessionStorage', 'clearAllSessionStorage',
229229
'getAllCookies', 'clearAllCookies',

packages/driver/cypress/fixtures/input_events.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
</head>
1313

1414
<body>
15-
<input type="text" id="focus" />
1615
<input type="text" id="keyup" />
1716
<input type="text" id="keydown" />
1817
</body>

packages/driver/src/cy/commands/actions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as Submit from './submit'
99
import * as Type from './type'
1010
import * as Trigger from './trigger'
1111
import * as Mount from './mount'
12+
import Press from './press'
1213

1314
export {
1415
Check,
@@ -22,4 +23,5 @@ export {
2223
Type,
2324
Trigger,
2425
Mount,
26+
Press,
2527
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import type { $Cy } from '../../../cypress/cy'
2+
import type { StateFunc } from '../../../cypress/state'
3+
import type { KeyPressSupportedKeys, AutomationCommands } from '@packages/types'
4+
import { defaults } from 'lodash'
5+
import { isSupportedKey } from '@packages/server/lib/automation/commands/key_press'
6+
import $errUtils from '../../../cypress/error_utils'
7+
import $utils from '../../../cypress/utils'
8+
9+
export interface PressCommand {
10+
(key: KeyPressSupportedKeys, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>): void
11+
}
12+
13+
export default function (Commands: Cypress.Commands, Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc, config: any) {
14+
async function pressCommand (key: KeyPressSupportedKeys, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>) {
15+
const options: Cypress.Loggable & Partial<Cypress.Timeoutable> = defaults({}, userOptions, {
16+
log: true,
17+
})
18+
const deltaOptions = $utils.filterOutOptions(options)
19+
20+
const log = Cypress.log({
21+
timeout: options.timeout,
22+
hidden: options.log === false,
23+
message: [key, deltaOptions],
24+
consoleProps () {
25+
return {
26+
'Key': key,
27+
}
28+
},
29+
})
30+
31+
if (!isSupportedKey(key)) {
32+
$errUtils.throwErrByPath('press.invalid_key', {
33+
onFail: log,
34+
args: { key },
35+
})
36+
37+
// throwErrByPath always throws, but there's no way to indicate that
38+
// code beyond this point is unreachable to typescript / linters
39+
return
40+
}
41+
42+
if (Cypress.browser.family === 'webkit') {
43+
$errUtils.throwErrByPath('press.unsupported_browser', {
44+
onFail: log,
45+
args: {
46+
family: Cypress.browser.family,
47+
},
48+
})
49+
50+
return
51+
}
52+
53+
if (Cypress.browser.name === 'firefox' && Number(Cypress.browser.majorVersion) < 135) {
54+
$errUtils.throwErrByPath('press.unsupported_browser_version', {
55+
onFail: log,
56+
args: {
57+
browser: Cypress.browser.name,
58+
version: Cypress.browser.majorVersion,
59+
minimumVersion: 135,
60+
},
61+
})
62+
}
63+
64+
try {
65+
const command: 'key:press' = 'key:press'
66+
const args: AutomationCommands[typeof command]['dataType'] = {
67+
key,
68+
}
69+
70+
await Cypress.automation('key:press', args)
71+
} catch (err) {
72+
$errUtils.throwErr(err, { onFail: log })
73+
}
74+
}
75+
76+
return Commands.add('press', pressCommand)
77+
}

0 commit comments

Comments
 (0)