mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-05 14:09:46 -06:00
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 <jennifer@cypress.io>
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
|
||||
## 14.2.2
|
||||
## 14.3.0
|
||||
|
||||
_Released 4/8/2025 (PENDING)_
|
||||
|
||||
**Features:**
|
||||
|
||||
- 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).
|
||||
|
||||
**Bugfixes:**
|
||||
|
||||
- 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).
|
||||
|
||||
201
cli/types/cypress.d.ts
vendored
201
cli/types/cypress.d.ts
vendored
@@ -578,99 +578,7 @@ declare namespace Cypress {
|
||||
*/
|
||||
stop(): void
|
||||
|
||||
Commands: {
|
||||
/**
|
||||
* Add a custom command
|
||||
* @see https://on.cypress.io/api/commands
|
||||
*/
|
||||
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
|
||||
|
||||
/**
|
||||
* Add a custom parent command
|
||||
* @see https://on.cypress.io/api/commands#Parent-Commands
|
||||
*/
|
||||
add<T extends keyof Chainable>(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn<T>): void
|
||||
|
||||
/**
|
||||
* Add a custom child command
|
||||
* @see https://on.cypress.io/api/commands#Child-Commands
|
||||
*/
|
||||
add<T extends keyof Chainable, S = any>(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject<T, S>): void
|
||||
|
||||
/**
|
||||
* Add a custom child or dual command
|
||||
* @see https://on.cypress.io/api/commands#Validations
|
||||
*/
|
||||
add<T extends keyof Chainable, S extends PrevSubject>(
|
||||
name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add a custom command that allows multiple types as the prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types
|
||||
*/
|
||||
add<T extends keyof Chainable, S extends PrevSubject>(
|
||||
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands
|
||||
* @see https://on.cypress.io/api/commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable>(fns: CommandFns): void
|
||||
|
||||
/**
|
||||
* Add one or more custom parent commands
|
||||
* @see https://on.cypress.io/api/commands#Parent-Commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable>(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void
|
||||
|
||||
/**
|
||||
* Add one or more custom child commands
|
||||
* @see https://on.cypress.io/api/commands#Child-Commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S = any>(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject<S>): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands that validate their prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Validations
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S extends PrevSubject>(
|
||||
options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands that allow multiple types as their prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Allow-Multiple-Types
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S extends PrevSubject>(
|
||||
options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject<PrevSubjectMap<void>[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress command with a new implementation
|
||||
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
|
||||
*/
|
||||
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress command with a new implementation
|
||||
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
|
||||
*/
|
||||
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
|
||||
|
||||
/**
|
||||
* Add a custom query
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress query with a new implementation
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): void
|
||||
}
|
||||
Commands: Commands
|
||||
|
||||
/**
|
||||
* @see https://on.cypress.io/cookies
|
||||
@@ -775,6 +683,9 @@ declare namespace Cypress {
|
||||
*/
|
||||
Keyboard: {
|
||||
defaults(options: Partial<KeyboardDefaultsOptions>): void
|
||||
Keys: {
|
||||
TAB: 'Tab',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -829,6 +740,100 @@ declare namespace Cypress {
|
||||
onSpecWindow: (window: Window, specList: string[] | Array<() => Promise<void>>) => void
|
||||
}
|
||||
|
||||
interface Commands {
|
||||
/**
|
||||
* Add a custom command
|
||||
* @see https://on.cypress.io/api/commands
|
||||
*/
|
||||
add<T extends keyof Chainable>(name: T, fn: CommandFn<T>): void
|
||||
|
||||
/**
|
||||
* Add a custom parent command
|
||||
* @see https://on.cypress.io/api/commands#Parent-Commands
|
||||
*/
|
||||
add<T extends keyof Chainable>(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn<T>): void
|
||||
|
||||
/**
|
||||
* Add a custom child command
|
||||
* @see https://on.cypress.io/api/commands#Child-Commands
|
||||
*/
|
||||
add<T extends keyof Chainable, S = any>(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject<T, S>): void
|
||||
|
||||
/**
|
||||
* Add a custom child or dual command
|
||||
* @see https://on.cypress.io/api/commands#Validations
|
||||
*/
|
||||
add<T extends keyof Chainable, S extends PrevSubject>(
|
||||
name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject<T, PrevSubjectMap[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add a custom command that allows multiple types as the prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types
|
||||
*/
|
||||
add<T extends keyof Chainable, S extends PrevSubject>(
|
||||
name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject<T, PrevSubjectMap<void>[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands
|
||||
* @see https://on.cypress.io/api/commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable>(fns: CommandFns): void
|
||||
|
||||
/**
|
||||
* Add one or more custom parent commands
|
||||
* @see https://on.cypress.io/api/commands#Parent-Commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable>(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void
|
||||
|
||||
/**
|
||||
* Add one or more custom child commands
|
||||
* @see https://on.cypress.io/api/commands#Child-Commands
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S = any>(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject<S>): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands that validate their prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Validations
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S extends PrevSubject>(
|
||||
options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject<PrevSubjectMap[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Add one or more custom commands that allow multiple types as their prevSubject
|
||||
* @see https://on.cypress.io/api/commands#Allow-Multiple-Types
|
||||
*/
|
||||
addAll<T extends keyof Chainable, S extends PrevSubject>(
|
||||
options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject<PrevSubjectMap<void>[S]>,
|
||||
): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress command with a new implementation
|
||||
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
|
||||
*/
|
||||
overwrite<T extends keyof Chainable>(name: T, fn: CommandFnWithOriginalFn<T>): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress command with a new implementation
|
||||
* @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands
|
||||
*/
|
||||
overwrite<T extends keyof Chainable, S extends PrevSubject>(name: T, fn: CommandFnWithOriginalFnAndSubject<T, PrevSubjectMap[S]>): void
|
||||
|
||||
/**
|
||||
* Add a custom query
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress query with a new implementation
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): void
|
||||
}
|
||||
|
||||
type CanReturnChainable = void | Chainable | Promise<unknown>
|
||||
type ThenReturn<S, R> =
|
||||
R extends void ? Chainable<S> :
|
||||
@@ -1742,6 +1747,16 @@ declare namespace Cypress {
|
||||
*/
|
||||
pause(options?: Partial<Loggable>): Chainable<Subject>
|
||||
|
||||
/**
|
||||
* Send a native sequence of keyboard events: keydown & press, followed by keyup, for the provided key.
|
||||
* Supported keys index the Cypress.Keyboard.Keys record.
|
||||
*
|
||||
* @example
|
||||
* cy.press(Cypress.Keyboard.Keys.TAB) // dispatches a keydown and press event to the browser, followed by a keyup event.
|
||||
* @see https://on.cypress.io/press
|
||||
*/
|
||||
press(key: typeof Cypress.Keyboard.Keys[keyof typeof Cypress.Keyboard.Keys], options?: Partial<Loggable & Timeoutable>): void
|
||||
|
||||
/**
|
||||
* Get the immediately preceding sibling of each element in a set of the elements.
|
||||
*
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"jsdoc-format": false,
|
||||
// for now keep the Cypress NPM module API
|
||||
// in its own file for simplicity
|
||||
"no-single-declare-module": false
|
||||
"no-single-declare-module": false,
|
||||
// This is detecting necessary qualifiers as unnecessary
|
||||
"no-unnecessary-qualifier": false
|
||||
},
|
||||
"linterOptions": {
|
||||
"exclude": [
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
describe('__placeholder__/commands/actions/press', () => {
|
||||
describe('src/cy/commands/actions/press', () => {
|
||||
it('dispatches the tab keypress to the AUT', () => {
|
||||
// Non-BiDi firefox is not supported
|
||||
if (Cypress.browser.family === 'firefox' && Cypress.browserMajorVersion() < 135) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Webkit is not supported. https://github.com/cypress-io/cypress/issues/31054
|
||||
if (Cypress.isBrowser('webkit')) {
|
||||
return
|
||||
}
|
||||
|
||||
cy.visit('/fixtures/input_events.html')
|
||||
|
||||
cy.get('#focus').focus().then(async () => {
|
||||
try {
|
||||
await Cypress.automation('key:press', { key: 'Tab' })
|
||||
} catch (e) {
|
||||
if (e.message && (e.message as string).includes('key:press')) {
|
||||
cy.log(e.message)
|
||||
cy.press(Cypress.Keyboard.Keys.TAB)
|
||||
|
||||
return
|
||||
}
|
||||
cy.get('#keydown').should('have.value', 'Tab')
|
||||
|
||||
throw e
|
||||
}
|
||||
|
||||
cy.get('#keyup').should('have.value', 'Tab')
|
||||
|
||||
cy.get('#keydown').should('have.value', 'Tab')
|
||||
})
|
||||
cy.get('#keyup').should('have.value', 'Tab')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -223,7 +223,7 @@ it('verifies number of cy commands', () => {
|
||||
'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile',
|
||||
'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit',
|
||||
'focused', 'get', 'contains', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
|
||||
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
|
||||
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', 'press',
|
||||
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
|
||||
'mount', 'as', 'root', 'getAllLocalStorage', 'clearAllLocalStorage', 'getAllSessionStorage', 'clearAllSessionStorage',
|
||||
'getAllCookies', 'clearAllCookies',
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<input type="text" id="focus" />
|
||||
<input type="text" id="keyup" />
|
||||
<input type="text" id="keydown" />
|
||||
</body>
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as Submit from './submit'
|
||||
import * as Type from './type'
|
||||
import * as Trigger from './trigger'
|
||||
import * as Mount from './mount'
|
||||
import Press from './press'
|
||||
|
||||
export {
|
||||
Check,
|
||||
@@ -22,4 +23,5 @@ export {
|
||||
Type,
|
||||
Trigger,
|
||||
Mount,
|
||||
Press,
|
||||
}
|
||||
|
||||
77
packages/driver/src/cy/commands/actions/press.ts
Normal file
77
packages/driver/src/cy/commands/actions/press.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { $Cy } from '../../../cypress/cy'
|
||||
import type { StateFunc } from '../../../cypress/state'
|
||||
import type { KeyPressSupportedKeys, AutomationCommands } from '@packages/types'
|
||||
import { defaults } from 'lodash'
|
||||
import { isSupportedKey } from '@packages/server/lib/automation/commands/key_press'
|
||||
import $errUtils from '../../../cypress/error_utils'
|
||||
import $utils from '../../../cypress/utils'
|
||||
|
||||
export interface PressCommand {
|
||||
(key: KeyPressSupportedKeys, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>): void
|
||||
}
|
||||
|
||||
export default function (Commands: Cypress.Commands, Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc, config: any) {
|
||||
async function pressCommand (key: KeyPressSupportedKeys, userOptions?: Partial<Cypress.Loggable> & Partial<Cypress.Timeoutable>) {
|
||||
const options: Cypress.Loggable & Partial<Cypress.Timeoutable> = defaults({}, userOptions, {
|
||||
log: true,
|
||||
})
|
||||
const deltaOptions = $utils.filterOutOptions(options)
|
||||
|
||||
const log = Cypress.log({
|
||||
timeout: options.timeout,
|
||||
hidden: options.log === false,
|
||||
message: [key, deltaOptions],
|
||||
consoleProps () {
|
||||
return {
|
||||
'Key': key,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (!isSupportedKey(key)) {
|
||||
$errUtils.throwErrByPath('press.invalid_key', {
|
||||
onFail: log,
|
||||
args: { key },
|
||||
})
|
||||
|
||||
// throwErrByPath always throws, but there's no way to indicate that
|
||||
// code beyond this point is unreachable to typescript / linters
|
||||
return
|
||||
}
|
||||
|
||||
if (Cypress.browser.family === 'webkit') {
|
||||
$errUtils.throwErrByPath('press.unsupported_browser', {
|
||||
onFail: log,
|
||||
args: {
|
||||
family: Cypress.browser.family,
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (Cypress.browser.name === 'firefox' && Number(Cypress.browser.majorVersion) < 135) {
|
||||
$errUtils.throwErrByPath('press.unsupported_browser_version', {
|
||||
onFail: log,
|
||||
args: {
|
||||
browser: Cypress.browser.name,
|
||||
version: Cypress.browser.majorVersion,
|
||||
minimumVersion: 135,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const command: 'key:press' = 'key:press'
|
||||
const args: AutomationCommands[typeof command]['dataType'] = {
|
||||
key,
|
||||
}
|
||||
|
||||
await Cypress.automation('key:press', args)
|
||||
} catch (err) {
|
||||
$errUtils.throwErr(err, { onFail: log })
|
||||
}
|
||||
}
|
||||
|
||||
return Commands.add('press', pressCommand)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import $utils from '../cypress/utils'
|
||||
import $window from '../dom/window'
|
||||
import type { Log } from '../cypress/log'
|
||||
import type { StateFunc } from '../cypress/state'
|
||||
import type { KeyPressSupportedKeys } from '@packages/types'
|
||||
|
||||
const debug = Debug('cypress:driver:keyboard')
|
||||
|
||||
@@ -1397,6 +1398,10 @@ const defaults = (props: Partial<Cypress.KeyboardDefaultsOptions>) => {
|
||||
return getConfig()
|
||||
}
|
||||
|
||||
const Keys: Record<string, KeyPressSupportedKeys> = {
|
||||
TAB: 'Tab',
|
||||
}
|
||||
|
||||
export default {
|
||||
defaults,
|
||||
getConfig,
|
||||
@@ -1405,4 +1410,5 @@ export default {
|
||||
reset,
|
||||
toModifiersEventOptions,
|
||||
fromModifierEventOptions,
|
||||
Keys,
|
||||
}
|
||||
|
||||
@@ -348,6 +348,7 @@ class $Cypress {
|
||||
this.downloads = $Downloads.create(this)
|
||||
|
||||
// wire up command create to cy
|
||||
// @ts-expect-error
|
||||
this.Commands = $Commands.create(this, this.cy, this.state, this.config)
|
||||
|
||||
this.events.proxyTo(this.cy)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { allCommands } from '../cy/commands'
|
||||
import { addCommand as addNetstubbingCommand } from '../cy/net-stubbing'
|
||||
import $errUtils from './error_utils'
|
||||
import $stackUtils from './stack_utils'
|
||||
|
||||
import type { $Cy } from './cy'
|
||||
import type { QueryFunction } from './state'
|
||||
|
||||
const PLACEHOLDER_COMMANDS = ['mount', 'hover']
|
||||
@@ -40,7 +40,7 @@ const internalError = (path, args) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
create: (Cypress, cy, state, config) => {
|
||||
create: (Cypress: Cypress.Cypress, cy: $Cy, state, config) => {
|
||||
const reservedCommandNames = new Set(Object.keys(cy))
|
||||
const commands = {}
|
||||
const queries = {}
|
||||
@@ -63,11 +63,11 @@ export default {
|
||||
return null
|
||||
},
|
||||
|
||||
addSync (name, fn) {
|
||||
addSync (name: string, fn: (...args: any[]) => any) {
|
||||
return cy.addCommandSync(name, fn)
|
||||
},
|
||||
|
||||
addAll (options = {}, obj) {
|
||||
addAll (options, obj) {
|
||||
if (!obj) {
|
||||
obj = options
|
||||
options = {}
|
||||
|
||||
@@ -1310,6 +1310,18 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
press: {
|
||||
invalid_key: stripIndent`\
|
||||
\`{{key}}\` is not supported by ${cmd('press')}. See \`Cypress.Keyboard.Keys\` for keys that are supported.
|
||||
`,
|
||||
unsupported_browser_version: stripIndent`\
|
||||
${cmd('press')} is not supported in {{browser}} version {{version}}. Upgrade to version {{minimumVersion}} to use \`cy.press()\`.
|
||||
`,
|
||||
unsupported_browser: stripIndent`\
|
||||
${cmd('press')} is not supported in {{family}} browsers.
|
||||
`,
|
||||
},
|
||||
|
||||
proxy: {
|
||||
js_rewriting_failed: stripIndent`\
|
||||
An error occurred in the Cypress proxy layer while rewriting your source code. This is a bug in Cypress. Open an issue if you see this message.
|
||||
|
||||
11
packages/driver/test/__setup__/setupMocks.ts
Normal file
11
packages/driver/test/__setup__/setupMocks.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { vi } from 'vitest'
|
||||
import type sourceMapUtils from '../../src/cypress/source_map_utils'
|
||||
|
||||
// This is mocked in the setup file because vitest chokes on loading the .wasm file
|
||||
// from the 'source-map' module. A solution to that should be found before unit testing
|
||||
// source_map_utils.
|
||||
vi.mock('../../src/cypress/source_map_utils', () => {
|
||||
return {
|
||||
getSourcePosition: vi.fn<typeof sourceMapUtils.getSourcePosition>(),
|
||||
}
|
||||
})
|
||||
187
packages/driver/test/unit/cy/commands/actions/press.spec.ts
Normal file
187
packages/driver/test/unit/cy/commands/actions/press.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { vi, describe, it, expect, beforeEach, Mock, MockedObject } from 'vitest'
|
||||
import type { KeyPressSupportedKeys } from '@packages/types'
|
||||
import addCommand, { PressCommand } from '../../../../../src/cy/commands/actions/press'
|
||||
import type { $Cy } from '../../../../../src/cypress/cy'
|
||||
import type { StateFunc } from '../../../../../src/cypress/state'
|
||||
import $errUtils from '../../../../../src/cypress/error_utils'
|
||||
import Keyboard from '../../../../../src/cy/keyboard'
|
||||
|
||||
vi.mock('../../../../../src/cypress/error_utils', async () => {
|
||||
const original = await vi.importActual('../../../../../src/cypress/error_utils')
|
||||
|
||||
return {
|
||||
default: {
|
||||
// @ts-expect-error
|
||||
...original.default,
|
||||
// @ts-expect-error
|
||||
throwErr: vi.fn().mockImplementation(original.default.throwErr),
|
||||
// @ts-expect-error
|
||||
throwErrByPath: vi.fn().mockImplementation(original.default.throwErrByPath),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('cy/commands/actions/press', () => {
|
||||
let log: Mock<typeof Cypress['log']>
|
||||
let automation: Mock<typeof Cypress['automation']>
|
||||
let press: PressCommand
|
||||
let Cypress: MockedObject<Cypress.Cypress>
|
||||
let Commands: MockedObject<Cypress.Commands>
|
||||
let cy: MockedObject<$Cy>
|
||||
let state: MockedObject<StateFunc>
|
||||
let config: any
|
||||
let logReturnValue: Cypress.Log
|
||||
|
||||
beforeEach(() => {
|
||||
log = vi.fn<typeof Cypress['log']>()
|
||||
automation = vi.fn<typeof Cypress['automation']>()
|
||||
|
||||
Cypress = {
|
||||
// The overloads for `log` don't get applied correctly here
|
||||
// @ts-expect-error
|
||||
log,
|
||||
automation,
|
||||
// @ts-expect-error
|
||||
browser: {
|
||||
family: 'chromium',
|
||||
name: 'Chrome',
|
||||
},
|
||||
}
|
||||
|
||||
// @ts-expect-error - this is a generic mock impl
|
||||
Commands = {
|
||||
add: vi.fn(),
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
cy = {}
|
||||
|
||||
state = {
|
||||
...vi.fn<StateFunc>(),
|
||||
// @ts-expect-error - this is a recursive definition, so we're only defining the mock one level deep
|
||||
state: vi.fn<StateFunc>(),
|
||||
reset: vi.fn<() => Record<string, any>>(),
|
||||
}
|
||||
|
||||
config = {}
|
||||
|
||||
logReturnValue = {
|
||||
id: 'log_id',
|
||||
end: vi.fn(),
|
||||
error: vi.fn(),
|
||||
finish: vi.fn(),
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
snapshot: vi.fn(),
|
||||
_hasInitiallyLogged: false,
|
||||
groupEnd: vi.fn(),
|
||||
}
|
||||
|
||||
Cypress.log.mockReturnValue(logReturnValue)
|
||||
|
||||
addCommand(Commands, Cypress, cy, state, config)
|
||||
|
||||
expect(Commands.add).toHaveBeenCalledOnce()
|
||||
|
||||
// @ts-expect-error
|
||||
const [[cmdName, cmd]]: [[string, PressCommand]] = Commands.add.mock.calls
|
||||
|
||||
expect(cmdName).toEqual('press')
|
||||
|
||||
press = cmd as PressCommand
|
||||
})
|
||||
|
||||
describe('with a valid key', () => {
|
||||
const key: KeyPressSupportedKeys = Keyboard.Keys.TAB
|
||||
|
||||
it('dispatches a key:press automation command', async () => {
|
||||
await press(key)
|
||||
expect(automation).toHaveBeenCalledWith('key:press', { key })
|
||||
})
|
||||
|
||||
describe('with options', () => {
|
||||
let options: Cypress.Loggable & Cypress.Timeoutable
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
log: false,
|
||||
timeout: 2000,
|
||||
}
|
||||
})
|
||||
|
||||
it('sets timeout and hidden on the log', async () => {
|
||||
await press(key, options)
|
||||
expect(Cypress.log).toBeCalledWith({
|
||||
timeout: options.timeout,
|
||||
hidden: true,
|
||||
message: [key, { timeout: 2000 }],
|
||||
consoleProps: expect.any(Function),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with an invalid key', () => {
|
||||
it('throws an invalid key error', async () => {
|
||||
// @ts-expect-error
|
||||
const key: KeyPressSupportedKeys = 'Foo'
|
||||
|
||||
await expect(press(key)).rejects.toThrow(`\`${key}\` is not supported by \`cy.press()\``)
|
||||
expect($errUtils.throwErrByPath).toHaveBeenCalledWith('press.invalid_key', {
|
||||
onFail: logReturnValue,
|
||||
args: {
|
||||
key,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when in webkit', () => {
|
||||
it('throws an unsupported browser error', async () => {
|
||||
Cypress.browser.family = 'webkit'
|
||||
await expect(press('Tab')).rejects.toThrow('`cy.press()` is not supported in webkit browsers.')
|
||||
expect($errUtils.throwErrByPath).toHaveBeenCalledWith('press.unsupported_browser', {
|
||||
onFail: logReturnValue,
|
||||
args: {
|
||||
family: Cypress.browser.family,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when in firefox below 135', () => {
|
||||
it('throws an unsupported browser version error', async () => {
|
||||
Cypress.browser.name = 'firefox'
|
||||
Cypress.browser.majorVersion = '134'
|
||||
await expect(press('Tab')).rejects.toThrow('`cy.press()` is not supported in firefox version 134. Upgrade to version 135 to use `cy.press()`.')
|
||||
|
||||
expect($errUtils.throwErrByPath).toHaveBeenCalledWith('press.unsupported_browser_version', {
|
||||
onFail: logReturnValue,
|
||||
args: {
|
||||
browser: Cypress.browser.name,
|
||||
version: Cypress.browser.majorVersion,
|
||||
minimumVersion: 135,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when automation throws', () => {
|
||||
it('throws via $errUtils, passing in the results from Cypress.log', async () => {
|
||||
const thrown = new Error('Some error')
|
||||
|
||||
// @ts-expect-error async is not bluebird, but that's fine
|
||||
Cypress.automation.mockImplementation(async () => {
|
||||
throw thrown
|
||||
})
|
||||
|
||||
await expect(press('Tab')).rejects.toThrow(thrown)
|
||||
expect($errUtils.throwErr).toHaveBeenCalledWith(thrown, {
|
||||
onFail: logReturnValue,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
3
packages/driver/types/internal-types.d.ts
vendored
3
packages/driver/types/internal-types.d.ts
vendored
@@ -31,6 +31,9 @@ interface InternalCheckOptions extends Partial<Cypress.CheckClearOptions> {
|
||||
interface InternalKeyboard extends Partial<Keyboard> {
|
||||
getMap: () => object
|
||||
reset: () => void
|
||||
Keys: {
|
||||
TAB: 'Tab'
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Cypress {
|
||||
|
||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
||||
include: ['test/unit/**/*.spec.ts'],
|
||||
environment: 'jsdom',
|
||||
exclude: ['**/__fixtures__/**/*'],
|
||||
setupFiles: 'test/__setup__/setupMocks.ts',
|
||||
reporters: [
|
||||
'default',
|
||||
['junit', { suiteName: 'Driver Unit Tests', outputFile: '/tmp/cypress/junit/driver-test-results.xml' }],
|
||||
|
||||
@@ -86,12 +86,12 @@ export class Automation {
|
||||
const onReq = this.get('onRequest')
|
||||
|
||||
if (onReq) {
|
||||
debug('Middleware `onRequest` fn found, attempting middleware exec for message: %s', message)
|
||||
|
||||
return Bluebird.try(() => {
|
||||
return onReq(resolvedMessage, resolvedData)
|
||||
}).catch((e) => {
|
||||
if (AutomationNotImplemented.isAutomationNotImplementedErr(e)) {
|
||||
debug(`${e.message}. Falling back to emit via socket.`)
|
||||
|
||||
return this.requestAutomationResponse(resolvedMessage, resolvedData, fn)
|
||||
}
|
||||
|
||||
@@ -189,6 +189,8 @@ export class Automation {
|
||||
}
|
||||
|
||||
use (middlewares: AutomationMiddleware) {
|
||||
debug('installing middleware')
|
||||
|
||||
return this.middleware = {
|
||||
...this.middleware,
|
||||
...middlewares,
|
||||
@@ -196,6 +198,7 @@ export class Automation {
|
||||
}
|
||||
|
||||
async push<T extends keyof AutomationCommands> (message: T, data: AutomationCommands[T]['dataType']) {
|
||||
debug('push `%s`: %o', message, data)
|
||||
const result = await this.normalize(message, data)
|
||||
|
||||
if (result) {
|
||||
@@ -206,6 +209,7 @@ export class Automation {
|
||||
async request<T extends keyof AutomationCommands> (message: T, data: AutomationCommands[T]['dataType'], fn) {
|
||||
// curry in the message + callback function
|
||||
// for obtaining the external automation data
|
||||
debug('request: `%s`', message)
|
||||
const automate = this.automationValve(message, fn)
|
||||
|
||||
await this.invokeAsync('onBeforeRequest', message, data)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping'
|
||||
import type { Protocol } from 'devtools-protocol'
|
||||
import type { KeyPressParams, KeyPressSupportedKeys } from '@packages/types'
|
||||
import type { SendDebuggerCommand } from '../../browsers/cdp_automation'
|
||||
import type { Client } from 'webdriver'
|
||||
|
||||
import Debug from 'debug'
|
||||
import { isEqual, isError } from 'lodash'
|
||||
|
||||
const debug = Debug('cypress:server:automation:command:keypress')
|
||||
|
||||
@@ -20,11 +22,41 @@ export class InvalidKeyError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export function isSupportedKey (key: string): key is KeyPressSupportedKeys {
|
||||
return CDP_KEYCODE[key] && BIDI_VALUE[key]
|
||||
}
|
||||
|
||||
export const CDP_KEYCODE: KeyCodeLookup = {
|
||||
'Tab': 'U+000009',
|
||||
}
|
||||
|
||||
export async function cdpKeyPress ({ key }: KeyPressParams, send: SendDebuggerCommand): Promise<void> {
|
||||
async function evaluateInFrameContext (expression: string,
|
||||
send: SendDebuggerCommand,
|
||||
contexts: Map<Protocol.Runtime.ExecutionContextId, Protocol.Runtime.ExecutionContextDescription>,
|
||||
frame: Protocol.Page.Frame): Promise<ProtocolMapping.Commands['Runtime.evaluate']['returnType']> {
|
||||
for (const [contextId, context] of contexts.entries()) {
|
||||
if (context.auxData?.frameId === frame.id) {
|
||||
try {
|
||||
return await send('Runtime.evaluate', {
|
||||
expression,
|
||||
contextId,
|
||||
})
|
||||
} catch (e) {
|
||||
if (isError(e) && (e as Error).message.includes('Cannot find context with specified id')) {
|
||||
debug('found invalid context %d, removing', contextId)
|
||||
contexts.delete(contextId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('Unable to find valid context for frame')
|
||||
}
|
||||
|
||||
export async function cdpKeyPress (
|
||||
{ key }: KeyPressParams, send: SendDebuggerCommand,
|
||||
contexts: Map<Protocol.Runtime.ExecutionContextId, Protocol.Runtime.ExecutionContextDescription>,
|
||||
frameTree: Protocol.Page.FrameTree,
|
||||
): Promise<void> {
|
||||
debug('cdp keypress', { key })
|
||||
if (!CDP_KEYCODE[key]) {
|
||||
throw new InvalidKeyError(key)
|
||||
@@ -32,6 +64,22 @@ export async function cdpKeyPress ({ key }: KeyPressParams, send: SendDebuggerCo
|
||||
|
||||
const keyIdentifier = CDP_KEYCODE[key]
|
||||
|
||||
const autFrame = frameTree.childFrames?.find(({ frame }) => {
|
||||
return frame.name?.includes('Your project')
|
||||
})
|
||||
|
||||
if (!autFrame) {
|
||||
throw new Error('Could not find AUT frame')
|
||||
}
|
||||
|
||||
const topActiveElement = await evaluateInFrameContext('document.activeElement', send, contexts, frameTree.frame)
|
||||
|
||||
const autFrameIsActive = topActiveElement.result.description && autFrame.frame.name && topActiveElement.result.description.includes(autFrame.frame.name)
|
||||
|
||||
if (!autFrameIsActive) {
|
||||
await evaluateInFrameContext('window.focus()', send, contexts, autFrame.frame)
|
||||
}
|
||||
|
||||
try {
|
||||
await send('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
@@ -56,19 +104,32 @@ export const BIDI_VALUE: KeyCodeLookup = {
|
||||
'Tab': '\uE004',
|
||||
}
|
||||
|
||||
export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, context: string, idSuffix?: string): Promise<void> {
|
||||
export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, autContext: string, idSuffix?: string): Promise<void> {
|
||||
const value = BIDI_VALUE[key]
|
||||
|
||||
if (!value) {
|
||||
throw new InvalidKeyError(key)
|
||||
}
|
||||
|
||||
const autFrameElement = await client.findElement('css selector', 'iframe.aut-iframe')
|
||||
const activeElement = await client.getActiveElement()
|
||||
|
||||
if (!isEqual(autFrameElement, activeElement)) {
|
||||
await client.scriptEvaluate(
|
||||
{
|
||||
expression: `window.focus()`,
|
||||
target: { context: autContext },
|
||||
awaitPromise: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await client.inputPerformActions({
|
||||
context,
|
||||
context: autContext,
|
||||
actions: [{
|
||||
type: 'key',
|
||||
id: `${context}-${key}-${idSuffix || Date.now()}`,
|
||||
id: `${autContext}-${key}-${idSuffix || Date.now()}`,
|
||||
actions: [
|
||||
{ type: 'keyDown', value },
|
||||
{ type: 'keyUp', value },
|
||||
|
||||
@@ -79,9 +79,9 @@ export class BidiAutomation {
|
||||
private interceptId: string | undefined = undefined
|
||||
|
||||
private constructor (webDriverClient: WebDriverClient, automation: Automation) {
|
||||
debug('initializing bidi automation')
|
||||
this.automation = automation
|
||||
this.webDriverClient = webDriverClient
|
||||
|
||||
// bind Bidi Events to update the standard automation client
|
||||
// Error here is expected until webdriver adds initiatorType and destination to the request object
|
||||
// @ts-expect-error
|
||||
@@ -91,9 +91,6 @@ export class BidiAutomation {
|
||||
this.webDriverClient.on('network.fetchError', this.onFetchError)
|
||||
this.webDriverClient.on('browsingContext.contextCreated', this.onBrowsingContextCreated)
|
||||
this.webDriverClient.on('browsingContext.contextDestroyed', this.onBrowsingContextDestroyed)
|
||||
|
||||
debug('registering middleware')
|
||||
automation.use(this.automationMiddleware)
|
||||
}
|
||||
|
||||
setTopLevelContextId = (contextId?: string) => {
|
||||
@@ -294,10 +291,10 @@ export class BidiAutomation {
|
||||
|
||||
switch (message) {
|
||||
case 'key:press':
|
||||
if (this.topLevelContextId) {
|
||||
await bidiKeyPress(data, this.webDriverClient, this.topLevelContextId)
|
||||
if (this.autContextId) {
|
||||
await bidiKeyPress(data, this.webDriverClient, this.autContextId, this.topLevelContextId)
|
||||
} else {
|
||||
throw new Error('Cannot emit key press: no top level context initialized')
|
||||
throw new Error('Cannot emit key press: no AUT context initialized')
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -169,6 +169,7 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware {
|
||||
private frameTree: Protocol.Page.FrameTree | undefined
|
||||
private gettingFrameTree: Promise<void> | undefined | null
|
||||
private cachedDataUrlRequestIds: Set<string> = new Set()
|
||||
private executionContexts: Map<Protocol.Runtime.ExecutionContextId, Protocol.Runtime.ExecutionContextDescription> = new Map()
|
||||
|
||||
private constructor (private sendDebuggerCommandFn: SendDebuggerCommand, private onFn: OnFn, private offFn: OffFn, private sendCloseCommandFn: SendCloseCommand, private automation: Automation, private focusTabOnScreenshot: boolean = false, private isHeadless: boolean = false) {
|
||||
onFn('Network.requestWillBeSent', this.onNetworkRequestWillBeSent)
|
||||
@@ -178,6 +179,9 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware {
|
||||
onFn('ServiceWorker.workerRegistrationUpdated', this.onServiceWorkerRegistrationUpdated)
|
||||
onFn('ServiceWorker.workerVersionUpdated', this.onServiceWorkerVersionUpdated)
|
||||
|
||||
onFn('Runtime.executionContextCreated', this.onExecutionContextCreated)
|
||||
onFn('Runtime.executionContextDestroyed', this.onExecutionContextDestroyed)
|
||||
|
||||
this.on = onFn
|
||||
this.off = offFn
|
||||
this.send = sendDebuggerCommandFn
|
||||
@@ -337,6 +341,18 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware {
|
||||
this.automation.onServiceWorkerVersionUpdated?.(params)
|
||||
}
|
||||
|
||||
private onExecutionContextCreated = (event: Protocol.Runtime.ExecutionContextCreatedEvent) => {
|
||||
debugVerbose('new execution context:', event)
|
||||
this.executionContexts.set(event.context.id, event.context)
|
||||
}
|
||||
|
||||
private onExecutionContextDestroyed = (event: Protocol.Runtime.ExecutionContextDestroyedEvent) => {
|
||||
debugVerbose('removing execution context', event)
|
||||
if (this.executionContexts.has(event.executionContextId)) {
|
||||
this.executionContexts.delete(event.executionContextId)
|
||||
}
|
||||
}
|
||||
|
||||
private getAllCookies = (filter: CyCookieFilter) => {
|
||||
return this.sendDebuggerCommandFn('Network.getAllCookies')
|
||||
.then((result: Protocol.Network.GetAllCookiesResponse) => {
|
||||
@@ -588,7 +604,13 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware {
|
||||
case 'collect:garbage':
|
||||
return this.sendDebuggerCommandFn('HeapProfiler.collectGarbage')
|
||||
case 'key:press':
|
||||
return cdpKeyPress(data, this.sendDebuggerCommandFn)
|
||||
if (this.gettingFrameTree) {
|
||||
debugVerbose('awaiting frame tree')
|
||||
|
||||
await this.gettingFrameTree
|
||||
}
|
||||
|
||||
return cdpKeyPress(data, this.sendDebuggerCommandFn, this.executionContexts, (await this.send('Page.getFrameTree')).frameTree)
|
||||
default:
|
||||
throw new Error(`No automation handler registered for: '${message}'`)
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ export default {
|
||||
// we need to set this to bind our AUT intercepts correctly. Hopefully we can move this in the future on a more sure implementation
|
||||
client.setTopLevelContextId(contexts[0].context)
|
||||
|
||||
automation.use(client.automationMiddleware)
|
||||
|
||||
await webdriverClient.browsingContextNavigate({
|
||||
context: contexts[0].context,
|
||||
url,
|
||||
|
||||
@@ -420,8 +420,13 @@ function shouldUseBiDi (browser: Browser): boolean {
|
||||
|
||||
export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
|
||||
if (shouldUseBiDi(browser)) {
|
||||
debug('connectToNewSpec bidi')
|
||||
await firefoxUtil.connectToNewSpecBiDi(options, automation, browserBidiClient!)
|
||||
|
||||
debug('registering middleware')
|
||||
automation.use(browserBidiClient!.automationMiddleware)
|
||||
} else {
|
||||
debug('connectToNewSpec cdp')
|
||||
await firefoxUtil.connectToNewSpecCDP(options, automation, browserCriClient!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,8 @@ export class SocketBase {
|
||||
message: T,
|
||||
data: AutomationCommands[T]['dataType'],
|
||||
) => {
|
||||
debug('request: %s', message)
|
||||
|
||||
return automation.request(message, data, onAutomationClientRequestCallback)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,32 +3,158 @@ import type { KeyPressSupportedKeys } from '@packages/types'
|
||||
import type { SendDebuggerCommand } from '../../../../lib/browsers/cdp_automation'
|
||||
import { cdpKeyPress, bidiKeyPress, BIDI_VALUE, CDP_KEYCODE } from '../../../../lib/automation/commands/key_press'
|
||||
import { Client as WebdriverClient } from 'webdriver'
|
||||
|
||||
import type { Protocol } from 'devtools-protocol'
|
||||
const { expect, sinon } = require('../../../spec_helper')
|
||||
|
||||
describe('key:press automation command', () => {
|
||||
describe('cdp()', () => {
|
||||
let sendFn: Sinon.SinonStub<Parameters<SendDebuggerCommand>, ReturnType<SendDebuggerCommand>>
|
||||
const topFrameId = 'abc'
|
||||
const autFrameId = 'def'
|
||||
|
||||
// @ts-expect-error
|
||||
const topExecutionContext: Protocol.Runtime.ExecutionContextDescription = {
|
||||
id: 123,
|
||||
auxData: {
|
||||
frameId: topFrameId,
|
||||
},
|
||||
}
|
||||
// @ts-expect-error
|
||||
const autExecutionContext: Protocol.Runtime.ExecutionContextDescription = {
|
||||
id: 456,
|
||||
auxData: {
|
||||
frameId: autFrameId,
|
||||
},
|
||||
}
|
||||
|
||||
let executionContexts: Map<Protocol.Runtime.ExecutionContextId, Protocol.Runtime.ExecutionContextDescription> = new Map()
|
||||
|
||||
const autFrame = {
|
||||
frame: {
|
||||
id: autFrameId,
|
||||
name: 'Your project',
|
||||
},
|
||||
}
|
||||
|
||||
const frameTree: Protocol.Page.FrameTree = {
|
||||
// @ts-expect-error - partial mock of the frame tree
|
||||
frame: {
|
||||
id: topFrameId,
|
||||
},
|
||||
childFrames: [
|
||||
// @ts-expect-error - partial mock of the frame tree
|
||||
autFrame,
|
||||
],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sendFn = sinon.stub()
|
||||
executionContexts.set(topExecutionContext.id, topExecutionContext)
|
||||
executionContexts.set(autExecutionContext.id, autExecutionContext)
|
||||
})
|
||||
|
||||
it('dispaches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => {
|
||||
await cdpKeyPress({ key: 'Tab' }, sendFn)
|
||||
describe('when the aut frame does not have focus', () => {
|
||||
const topActiveElement: Protocol.Runtime.EvaluateResponse = {
|
||||
result: {
|
||||
type: 'object',
|
||||
description: 'a.some-link',
|
||||
},
|
||||
}
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
beforeEach(() => {
|
||||
sendFn.withArgs('Runtime.evaluate', {
|
||||
expression: 'document.activeElement',
|
||||
contextId: topExecutionContext.id,
|
||||
}).resolves(topActiveElement)
|
||||
})
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
it('focuses the frame and sends keydown and keyup', async () => {
|
||||
await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree)
|
||||
expect(sendFn).to.have.been.calledWith('Runtime.evaluate', {
|
||||
expression: 'window.focus()',
|
||||
contextId: autExecutionContext.id,
|
||||
})
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
})
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are invalid execution contexts associated with the top frame', () => {
|
||||
// @ts-expect-error - this is a "fake" partial
|
||||
const invalidExecutionContext: Protocol.Runtime.ExecutionContextDescription = {
|
||||
id: 9,
|
||||
auxData: {
|
||||
frameId: topFrameId,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
executionContexts = new Map()
|
||||
executionContexts.set(invalidExecutionContext.id, invalidExecutionContext)
|
||||
executionContexts.set(topExecutionContext.id, topExecutionContext)
|
||||
executionContexts.set(autExecutionContext.id, autExecutionContext)
|
||||
sendFn.withArgs('Runtime.evaluate', {
|
||||
expression: 'document.activeElement',
|
||||
contextId: invalidExecutionContext.id,
|
||||
}).rejects(new Error('Cannot find context with specified id'))
|
||||
})
|
||||
|
||||
it('does not throw', async () => {
|
||||
let thrown: any = undefined
|
||||
|
||||
try {
|
||||
await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree)
|
||||
} catch (e) {
|
||||
thrown = e
|
||||
}
|
||||
|
||||
expect(thrown).to.be.undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the aut frame has focus', () => {
|
||||
const topActiveElement: Protocol.Runtime.EvaluateResponse = {
|
||||
result: {
|
||||
type: 'object',
|
||||
description: autFrame.frame.name,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sendFn.withArgs('Runtime.evaluate', {
|
||||
expression: 'document.activeElement',
|
||||
contextId: topExecutionContext.id,
|
||||
}).resolves(topActiveElement)
|
||||
})
|
||||
|
||||
it('dispaches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => {
|
||||
await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree)
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyDown',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
})
|
||||
|
||||
expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', {
|
||||
type: 'keyUp',
|
||||
keyIdentifier: CDP_KEYCODE.Tab,
|
||||
key: 'Tab',
|
||||
code: 'Tab',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,15 +163,21 @@ describe('key:press automation command', () => {
|
||||
// typescript would keep this from happening, but it hasn't yet
|
||||
// been checked for correctness since being received by automation
|
||||
// @ts-expect-error
|
||||
await expect(cdpKeyPress({ key: 'foo' })).to.be.rejectedWith('foo is not supported by \'cy.press()\'.')
|
||||
await expect(cdpKeyPress({ key: 'foo' }, sendFn, executionContexts, frameTree)).to.be.rejectedWith('foo is not supported by \'cy.press()\'.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('bidi', () => {
|
||||
let client: Sinon.SinonStubbedInstance<WebdriverClient>
|
||||
let context: string
|
||||
let autContext: string
|
||||
let key: KeyPressSupportedKeys
|
||||
const iframeElement = {
|
||||
'element-6066-11e4-a52e-4f735466cecf': 'uuid-1',
|
||||
}
|
||||
const otherElement = {
|
||||
'element-6066-11e4-a52e-4f735466cecf': 'uuid-2',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// can't create a sinon stubbed instance because webdriver doesn't export the constructor. Because it's known that
|
||||
@@ -53,18 +185,55 @@ describe('key:press automation command', () => {
|
||||
// @ts-expect-error
|
||||
client = {
|
||||
inputPerformActions: (sinon as Sinon.SinonSandbox).stub<Parameters<WebdriverClient['inputPerformActions']>, ReturnType<WebdriverClient['inputPerformActions']>>(),
|
||||
getActiveElement: (sinon as Sinon.SinonSandbox).stub<Parameters<WebdriverClient['getActiveElement']>, ReturnType<WebdriverClient['getActiveElement']>>(),
|
||||
findElement: (sinon as Sinon.SinonSandbox).stub<Parameters<WebdriverClient['findElement']>, ReturnType<WebdriverClient['findElement']>>(),
|
||||
scriptEvaluate: (sinon as Sinon.SinonSandbox).stub<Parameters<WebdriverClient['scriptEvaluate']>, ReturnType<WebdriverClient['scriptEvaluate']>>(),
|
||||
}
|
||||
|
||||
context = 'someContextId'
|
||||
autContext = 'someContextId'
|
||||
|
||||
key = 'Tab'
|
||||
|
||||
client.inputPerformActions.resolves()
|
||||
})
|
||||
|
||||
it('calls client.inputPerformActions with a keydown, pause, and keyup action', () => {
|
||||
bidiKeyPress({ key }, client as WebdriverClient, context, 'idSuffix')
|
||||
describe('when the aut iframe is not in focus', () => {
|
||||
beforeEach(() => {
|
||||
client.findElement.withArgs('css selector ', 'iframe.aut-iframe').resolves(iframeElement)
|
||||
// @ts-expect-error - webdriver types show this returning a string, but it actually returns an ElementReference, same as findElement
|
||||
client.getActiveElement.resolves(otherElement)
|
||||
})
|
||||
|
||||
it('focuses the frame before dispatching keydown and keyup', async () => {
|
||||
await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix')
|
||||
expect(client.scriptEvaluate).to.have.been.calledWith({
|
||||
expression: 'window.focus()',
|
||||
target: { context: autContext },
|
||||
awaitPromise: false,
|
||||
})
|
||||
|
||||
expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({
|
||||
context: autContext,
|
||||
actions: [{
|
||||
type: 'key',
|
||||
id: 'someContextId-Tab-idSuffix',
|
||||
actions: [
|
||||
{ type: 'keyDown', value: BIDI_VALUE[key] },
|
||||
{ type: 'keyUp', value: BIDI_VALUE[key] },
|
||||
],
|
||||
}],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('calls client.inputPerformActions with a keydown and keyup action', async () => {
|
||||
client.findElement.withArgs('css selector ', 'iframe.aut-iframe').resolves(iframeElement)
|
||||
// @ts-expect-error - webdriver types show this returning a string, but it actually returns an ElementReference, same as findElement
|
||||
client.getActiveElement.resolves(iframeElement)
|
||||
await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix')
|
||||
|
||||
expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({
|
||||
context,
|
||||
context: autContext,
|
||||
actions: [{
|
||||
type: 'key',
|
||||
id: 'someContextId-Tab-idSuffix',
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('lib/browsers/bidi_automation', () => {
|
||||
it('binds BIDI_EVENTS when a new instance is created', () => {
|
||||
mockWebdriverClient.on = sinon.stub()
|
||||
|
||||
const bidiAutoInstance = BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
BidiAutomation.create(mockWebdriverClient, mockAutomationClient)
|
||||
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.beforeRequestSent')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.responseStarted')
|
||||
@@ -41,7 +41,6 @@ describe('lib/browsers/bidi_automation', () => {
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('network.fetchError')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('browsingContext.contextCreated')
|
||||
expect(mockWebdriverClient.on).to.have.been.calledWith('browsingContext.contextDestroyed')
|
||||
expect(mockAutomationClient.use).to.have.been.calledWith(bidiAutoInstance.automationMiddleware)
|
||||
})
|
||||
|
||||
it('unbinds BIDI_EVENTS when close() is called', () => {
|
||||
|
||||
58
packages/server/test/unit/browsers/firefox-util_spec.ts
Normal file
58
packages/server/test/unit/browsers/firefox-util_spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
require('../../spec_helper')
|
||||
import FirefoxUtil from '../../../lib/browsers/firefox-util'
|
||||
import sinon from 'sinon'
|
||||
import { expect } from 'chai'
|
||||
import { Automation } from '../../../lib/automation'
|
||||
import { Client as WebDriverClient } from 'webdriver'
|
||||
import { BidiAutomation } from '../../../lib/browsers/bidi_automation'
|
||||
|
||||
describe('Firefox-Util', () => {
|
||||
let automation: sinon.SinonStubbedInstance<Automation>
|
||||
let onError: sinon.SinonStub<[Error], void>
|
||||
let url: string
|
||||
let remotePort: number | undefined
|
||||
let webdriverClient: Partial<sinon.SinonStubbedInstance<WebDriverClient>>
|
||||
let useWebDriverBiDi: boolean
|
||||
let stubbedBiDiAutomation: sinon.SinonStubbedInstance<BidiAutomation>
|
||||
|
||||
beforeEach(() => {
|
||||
automation = sinon.createStubInstance(Automation)
|
||||
onError = sinon.stub<[Error], void>()
|
||||
url = 'http://some-url'
|
||||
remotePort = 8000
|
||||
webdriverClient = {
|
||||
sessionSubscribe: sinon.stub<
|
||||
Parameters<WebDriverClient['sessionSubscribe']>,
|
||||
ReturnType<WebDriverClient['sessionSubscribe']>
|
||||
>().resolves(),
|
||||
browsingContextGetTree: sinon.stub<
|
||||
Parameters<WebDriverClient['browsingContextGetTree']>,
|
||||
ReturnType<WebDriverClient['browsingContextGetTree']>
|
||||
>().resolves({ contexts: [{
|
||||
context: 'abc',
|
||||
children: [],
|
||||
url: 'http://some-url',
|
||||
userContext: 'user-context',
|
||||
}] }),
|
||||
browsingContextNavigate: sinon.stub<
|
||||
Parameters<WebDriverClient['browsingContextNavigate']>,
|
||||
ReturnType<WebDriverClient['browsingContextNavigate']>
|
||||
>().resolves(),
|
||||
}
|
||||
|
||||
useWebDriverBiDi = true
|
||||
stubbedBiDiAutomation = sinon.createStubInstance(BidiAutomation)
|
||||
// sinon's createStubInstance doesn't stub out this member method
|
||||
stubbedBiDiAutomation.setTopLevelContextId = sinon.stub()
|
||||
sinon.stub(BidiAutomation, 'create').returns(stubbedBiDiAutomation)
|
||||
})
|
||||
|
||||
describe('.setup()', () => {
|
||||
describe('when using bidi', () => {
|
||||
it('registers the automation middleware with the automation system', async () => {
|
||||
await FirefoxUtil.setup({ automation, onError, url, remotePort, webdriverClient, useWebDriverBiDi })
|
||||
expect(automation.use).to.have.been.calledWith(stubbedBiDiAutomation.automationMiddleware)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -162,6 +162,8 @@ describe('lib/browsers/firefox', () => {
|
||||
context: mockContextId,
|
||||
url: 'next-spec-url',
|
||||
})
|
||||
|
||||
expect(this.automation.use).to.have.been.calledWith(bidiAutomationClient.automationMiddleware)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user