diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index f4b7c1e1d83..044cca7c501 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,8 +1,12 @@ -## 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). diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index b70d5839744..54d69512bc5 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -578,99 +578,7 @@ declare namespace Cypress { */ stop(): void - Commands: { - /** - * Add a custom command - * @see https://on.cypress.io/api/commands - */ - add(name: T, fn: CommandFn): void - - /** - * Add a custom parent command - * @see https://on.cypress.io/api/commands#Parent-Commands - */ - add(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn): void - - /** - * Add a custom child command - * @see https://on.cypress.io/api/commands#Child-Commands - */ - add(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject): void - - /** - * Add a custom child or dual command - * @see https://on.cypress.io/api/commands#Validations - */ - add( - name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, - ): void - - /** - * Add a custom command that allows multiple types as the prevSubject - * @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types - */ - add( - name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, - ): void - - /** - * Add one or more custom commands - * @see https://on.cypress.io/api/commands - */ - addAll(fns: CommandFns): void - - /** - * Add one or more custom parent commands - * @see https://on.cypress.io/api/commands#Parent-Commands - */ - addAll(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void - - /** - * Add one or more custom child commands - * @see https://on.cypress.io/api/commands#Child-Commands - */ - addAll(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject): void - - /** - * Add one or more custom commands that validate their prevSubject - * @see https://on.cypress.io/api/commands#Validations - */ - addAll( - options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, - ): 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( - options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, - ): void - - /** - * Overwrite an existing Cypress command with a new implementation - * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands - */ - overwrite(name: T, fn: CommandFnWithOriginalFn): void - - /** - * Overwrite an existing Cypress command with a new implementation - * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands - */ - overwrite(name: T, fn: CommandFnWithOriginalFnAndSubject): void - - /** - * Add a custom query - * @see https://on.cypress.io/api/custom-queries - */ - addQuery(name: T, fn: QueryFn): void - - /** - * Overwrite an existing Cypress query with a new implementation - * @see https://on.cypress.io/api/custom-queries - */ - overwriteQuery(name: T, fn: QueryFnWithOriginalFn): void - } + Commands: Commands /** * @see https://on.cypress.io/cookies @@ -775,6 +683,9 @@ declare namespace Cypress { */ Keyboard: { defaults(options: Partial): void + Keys: { + TAB: 'Tab', + }, } /** @@ -829,6 +740,100 @@ declare namespace Cypress { onSpecWindow: (window: Window, specList: string[] | Array<() => Promise>) => void } + interface Commands { + /** + * Add a custom command + * @see https://on.cypress.io/api/commands + */ + add(name: T, fn: CommandFn): void + + /** + * Add a custom parent command + * @see https://on.cypress.io/api/commands#Parent-Commands + */ + add(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn): void + + /** + * Add a custom child command + * @see https://on.cypress.io/api/commands#Child-Commands + */ + add(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject): void + + /** + * Add a custom child or dual command + * @see https://on.cypress.io/api/commands#Validations + */ + add( + name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, + ): void + + /** + * Add a custom command that allows multiple types as the prevSubject + * @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types + */ + add( + name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, + ): void + + /** + * Add one or more custom commands + * @see https://on.cypress.io/api/commands + */ + addAll(fns: CommandFns): void + + /** + * Add one or more custom parent commands + * @see https://on.cypress.io/api/commands#Parent-Commands + */ + addAll(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void + + /** + * Add one or more custom child commands + * @see https://on.cypress.io/api/commands#Child-Commands + */ + addAll(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject): void + + /** + * Add one or more custom commands that validate their prevSubject + * @see https://on.cypress.io/api/commands#Validations + */ + addAll( + options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, + ): 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( + options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, + ): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ + overwrite(name: T, fn: CommandFnWithOriginalFn): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ + overwrite(name: T, fn: CommandFnWithOriginalFnAndSubject): void + + /** + * Add a custom query + * @see https://on.cypress.io/api/custom-queries + */ + addQuery(name: T, fn: QueryFn): void + + /** + * Overwrite an existing Cypress query with a new implementation + * @see https://on.cypress.io/api/custom-queries + */ + overwriteQuery(name: T, fn: QueryFnWithOriginalFn): void + } + type CanReturnChainable = void | Chainable | Promise type ThenReturn = R extends void ? Chainable : @@ -1742,6 +1747,16 @@ declare namespace Cypress { */ pause(options?: Partial): Chainable + /** + * 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): void + /** * Get the immediately preceding sibling of each element in a set of the elements. * diff --git a/cli/types/tslint.json b/cli/types/tslint.json index 017eb3c2e3f..5e53c4842cf 100644 --- a/cli/types/tslint.json +++ b/cli/types/tslint.json @@ -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": [ diff --git a/packages/driver/cypress/e2e/commands/actions/press.cy.ts b/packages/driver/cypress/e2e/commands/actions/press.cy.ts index 7fc5f8694dc..1fcf76f5257 100644 --- a/packages/driver/cypress/e2e/commands/actions/press.cy.ts +++ b/packages/driver/cypress/e2e/commands/actions/press.cy.ts @@ -1,23 +1,21 @@ -describe('__placeholder__/commands/actions/press', () => { +describe('src/cy/commands/actions/press', () => { it('dispatches the tab keypress to the AUT', () => { - cy.visit('/fixtures/input_events.html') + // Non-BiDi firefox is not supported + if (Cypress.browser.family === 'firefox' && Cypress.browserMajorVersion() < 135) { + return + } - 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) + // TODO: Webkit is not supported. https://github.com/cypress-io/cypress/issues/31054 + if (Cypress.isBrowser('webkit')) { + return + } - return - } + cy.visit('/fixtures/input_events.html') - throw e - } + cy.press(Cypress.Keyboard.Keys.TAB) - cy.get('#keyup').should('have.value', 'Tab') + cy.get('#keydown').should('have.value', 'Tab') - cy.get('#keydown').should('have.value', 'Tab') - }) + cy.get('#keyup').should('have.value', 'Tab') }) }) diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts index 464813e4f3d..a58e07bda1e 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts @@ -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', diff --git a/packages/driver/cypress/fixtures/input_events.html b/packages/driver/cypress/fixtures/input_events.html index 568e4d8cb02..2a23c656452 100644 --- a/packages/driver/cypress/fixtures/input_events.html +++ b/packages/driver/cypress/fixtures/input_events.html @@ -12,7 +12,6 @@ - diff --git a/packages/driver/src/cy/commands/actions/index.ts b/packages/driver/src/cy/commands/actions/index.ts index fe8eb2123d0..18a11fd773e 100644 --- a/packages/driver/src/cy/commands/actions/index.ts +++ b/packages/driver/src/cy/commands/actions/index.ts @@ -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, } diff --git a/packages/driver/src/cy/commands/actions/press.ts b/packages/driver/src/cy/commands/actions/press.ts new file mode 100644 index 00000000000..5e96fa50326 --- /dev/null +++ b/packages/driver/src/cy/commands/actions/press.ts @@ -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 & Partial): void +} + +export default function (Commands: Cypress.Commands, Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc, config: any) { + async function pressCommand (key: KeyPressSupportedKeys, userOptions?: Partial & Partial) { + const options: Cypress.Loggable & Partial = 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) +} diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index fbeb454fe78..3256d9f951c 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -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) => { return getConfig() } +const Keys: Record = { + TAB: 'Tab', +} + export default { defaults, getConfig, @@ -1405,4 +1410,5 @@ export default { reset, toModifiersEventOptions, fromModifierEventOptions, + Keys, } diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index ea62efa511a..c8657904fae 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -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) diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts index 36bf924f308..9039c1186d7 100644 --- a/packages/driver/src/cypress/commands.ts +++ b/packages/driver/src/cypress/commands.ts @@ -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 = {} diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 34e998fe8d6..a82d7a41908 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -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. diff --git a/packages/driver/test/__setup__/setupMocks.ts b/packages/driver/test/__setup__/setupMocks.ts new file mode 100644 index 00000000000..282cd6cf150 --- /dev/null +++ b/packages/driver/test/__setup__/setupMocks.ts @@ -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(), + } +}) diff --git a/packages/driver/test/unit/cy/commands/actions/press.spec.ts b/packages/driver/test/unit/cy/commands/actions/press.spec.ts new file mode 100644 index 00000000000..2e77adae79f --- /dev/null +++ b/packages/driver/test/unit/cy/commands/actions/press.spec.ts @@ -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 + let automation: Mock + let press: PressCommand + let Cypress: MockedObject + let Commands: MockedObject + let cy: MockedObject<$Cy> + let state: MockedObject + let config: any + let logReturnValue: Cypress.Log + + beforeEach(() => { + log = vi.fn() + automation = vi.fn() + + 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(), + // @ts-expect-error - this is a recursive definition, so we're only defining the mock one level deep + state: vi.fn(), + reset: vi.fn<() => Record>(), + } + + 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, + }) + }) + }) +}) diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index 8c9a5c32bfe..a190a2a8e60 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -31,6 +31,9 @@ interface InternalCheckOptions extends Partial { interface InternalKeyboard extends Partial { getMap: () => object reset: () => void + Keys: { + TAB: 'Tab' + } } declare namespace Cypress { diff --git a/packages/driver/vitest.config.mjs b/packages/driver/vitest.config.mjs index 0549d3593ad..3f8f02a7a2d 100644 --- a/packages/driver/vitest.config.mjs +++ b/packages/driver/vitest.config.mjs @@ -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' }], diff --git a/packages/server/lib/automation/automation.ts b/packages/server/lib/automation/automation.ts index 7a7250efee9..b4073451dbc 100644 --- a/packages/server/lib/automation/automation.ts +++ b/packages/server/lib/automation/automation.ts @@ -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 (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 (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) diff --git a/packages/server/lib/automation/commands/key_press.ts b/packages/server/lib/automation/commands/key_press.ts index aa7b28241e2..5d556a7a743 100644 --- a/packages/server/lib/automation/commands/key_press.ts +++ b/packages/server/lib/automation/commands/key_press.ts @@ -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 { +async function evaluateInFrameContext (expression: string, + send: SendDebuggerCommand, + contexts: Map, + frame: Protocol.Page.Frame): Promise { + 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, + frameTree: Protocol.Page.FrameTree, +): Promise { 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 { +export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, autContext: string, idSuffix?: string): Promise { 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 }, diff --git a/packages/server/lib/browsers/bidi_automation.ts b/packages/server/lib/browsers/bidi_automation.ts index 4cdfbf1a0f1..b9329cefe18 100644 --- a/packages/server/lib/browsers/bidi_automation.ts +++ b/packages/server/lib/browsers/bidi_automation.ts @@ -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 diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index 3c6ecb6d2d3..1ef28e4a732 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -169,6 +169,7 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware { private frameTree: Protocol.Page.FrameTree | undefined private gettingFrameTree: Promise | undefined | null private cachedDataUrlRequestIds: Set = new Set() + private executionContexts: Map = 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}'`) } diff --git a/packages/server/lib/browsers/firefox-util.ts b/packages/server/lib/browsers/firefox-util.ts index 620260224ce..26f48d9ee15 100644 --- a/packages/server/lib/browsers/firefox-util.ts +++ b/packages/server/lib/browsers/firefox-util.ts @@ -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, diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index abc57a9bbf7..9cdc2c4e4c9 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -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!) } } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 7b14f55d94c..1c872846517 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -185,6 +185,8 @@ export class SocketBase { message: T, data: AutomationCommands[T]['dataType'], ) => { + debug('request: %s', message) + return automation.request(message, data, onAutomationClientRequestCallback) } diff --git a/packages/server/test/unit/automation/commands/key_press.spec.ts b/packages/server/test/unit/automation/commands/key_press.spec.ts index 10a2a6db85a..a62a4c927d8 100644 --- a/packages/server/test/unit/automation/commands/key_press.spec.ts +++ b/packages/server/test/unit/automation/commands/key_press.spec.ts @@ -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, ReturnType> + 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 = 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) + }) + + describe('when the aut frame does not have focus', () => { + const topActiveElement: Protocol.Runtime.EvaluateResponse = { + result: { + type: 'object', + description: 'a.some-link', + }, + } + + beforeEach(() => { + sendFn.withArgs('Runtime.evaluate', { + expression: 'document.activeElement', + contextId: topExecutionContext.id, + }).resolves(topActiveElement) + }) + + 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 + }) + }) }) - 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 has focus', () => { + const topActiveElement: Protocol.Runtime.EvaluateResponse = { + result: { + type: 'object', + description: autFrame.frame.name, + }, + } - 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('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 - 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, ReturnType>(), + getActiveElement: (sinon as Sinon.SinonSandbox).stub, ReturnType>(), + findElement: (sinon as Sinon.SinonSandbox).stub, ReturnType>(), + scriptEvaluate: (sinon as Sinon.SinonSandbox).stub, ReturnType>(), } - context = 'someContextId' + autContext = 'someContextId' key = 'Tab' + + client.inputPerformActions.resolves() + }) + + 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, pause, and keyup action', () => { - bidiKeyPress({ key }, client as WebdriverClient, context, 'idSuffix') + 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', diff --git a/packages/server/test/unit/browsers/bidi_automation_spec.ts b/packages/server/test/unit/browsers/bidi_automation_spec.ts index 75fffc44566..4f2575df9f6 100644 --- a/packages/server/test/unit/browsers/bidi_automation_spec.ts +++ b/packages/server/test/unit/browsers/bidi_automation_spec.ts @@ -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', () => { diff --git a/packages/server/test/unit/browsers/firefox-util_spec.ts b/packages/server/test/unit/browsers/firefox-util_spec.ts new file mode 100644 index 00000000000..08abf506f02 --- /dev/null +++ b/packages/server/test/unit/browsers/firefox-util_spec.ts @@ -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 + let onError: sinon.SinonStub<[Error], void> + let url: string + let remotePort: number | undefined + let webdriverClient: Partial> + let useWebDriverBiDi: boolean + let stubbedBiDiAutomation: sinon.SinonStubbedInstance + + beforeEach(() => { + automation = sinon.createStubInstance(Automation) + onError = sinon.stub<[Error], void>() + url = 'http://some-url' + remotePort = 8000 + webdriverClient = { + sessionSubscribe: sinon.stub< + Parameters, + ReturnType + >().resolves(), + browsingContextGetTree: sinon.stub< + Parameters, + ReturnType + >().resolves({ contexts: [{ + context: 'abc', + children: [], + url: 'http://some-url', + userContext: 'user-context', + }] }), + browsingContextNavigate: sinon.stub< + Parameters, + ReturnType + >().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) + }) + }) + }) +}) diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index a9b9e61ff74..a7d1b76c65c 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -162,6 +162,8 @@ describe('lib/browsers/firefox', () => { context: mockContextId, url: 'next-spec-url', }) + + expect(this.automation.use).to.have.been.calledWith(bidiAutomationClient.automationMiddleware) }) })