fix: activate main tab before detecting iframe focus in firefox/bidi implementation of cy.press() (#31481)

* activate main window before attempting to detect aut iframe active element

* add todo about refactor

* changelog

* system test for issue 31466

* no infinite max depth

* changelog

* changelog

* simplify

* additional test, better window switching logic
This commit is contained in:
Cacie Prins
2025-04-14 16:13:13 -04:00
committed by GitHub
parent 189bb9ee5f
commit 9a51a0ff45
8 changed files with 144 additions and 18 deletions
+4
View File
@@ -3,6 +3,10 @@
_Released 4/22/2025 (PENDING)_
**Bugfixes:**
- The [`cy.press()`](http://on.cypress.io/api/press) command no longer errors when used in specs subsequent to the first spec in run mode. Fixes [#31466](https://github.com/cypress-io/cypress/issues/31466).
**Misc:**
- The UI of the reporter and URL were updated to a darker gray background for better color contrast. Addressed in [#31475](https://github.com/cypress-io/cypress/pull/31475).
@@ -104,6 +104,14 @@ export const BIDI_VALUE: KeyCodeLookup = {
'Tab': '\uE004',
}
async function getActiveWindow (client: Client) {
try {
return await client.getWindowHandle()
} catch (e) {
return undefined
}
}
export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, autContext: string, idSuffix?: string): Promise<void> {
const value = BIDI_VALUE[key]
@@ -111,17 +119,41 @@ export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, aut
throw new InvalidKeyError(key)
}
const autFrameElement = await client.findElement('css selector', 'iframe.aut-iframe')
const activeElement = await client.getActiveElement()
const activeWindow = await getActiveWindow(client)
const { contexts: [{ context: topLevelContext }] } = await client.browsingContextGetTree({})
if (!isEqual(autFrameElement, activeElement)) {
await client.scriptEvaluate(
{
expression: `window.focus()`,
target: { context: autContext },
awaitPromise: false,
},
)
// TODO: refactor for Cy15 https://github.com/cypress-io/cypress/issues/31480
if (activeWindow !== topLevelContext) {
debug('Primary window is not currently active; attempting to activate')
try {
await client.switchToWindow(topLevelContext)
} catch (e) {
debug('Error while attempting to activate main browser tab:', e)
const err = new Error(`Unable to activate main browser tab: ${e?.message || 'Unknown Error Occurred'}. DEBUG namespace cypress:server:automation:command:keypress for more information.`)
throw err
}
}
try {
const autFrameElement = await client.findElement('css selector', 'iframe.aut-iframe')
const activeElement = await client.getActiveElement()
if (!isEqual(autFrameElement, activeElement)) {
debug('aut iframe is not currently focused; focusing aut iframe: ', autContext)
await client.scriptEvaluate(
{
expression: `window.focus()`,
target: { context: autContext },
awaitPromise: false,
},
)
}
} catch (e) {
debug('Error occurred during aut frame focus detection:', e)
const err = new Error(`Unable to focus the AUT iframe: ${e?.message || 'Unknown Error Occurred'}. DEBUG namespace cypress:server:automation:command:keypress for more information.`)
throw err
}
try {
@@ -138,6 +170,8 @@ export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, aut
})
} catch (e) {
debug(e)
throw e
const err = new Error(`Unable to perform key press command for '${key}' key: ${e?.message || 'Unknown Error Occurred'}. DEBUG namespace cypress:server:automation:command:keypress for more information.`)
throw err
}
}
@@ -288,7 +288,7 @@ export class BidiAutomation {
public readonly automationMiddleware: AutomationMiddleware = {
onRequest: async <T extends keyof AutomationCommands> (message: T, data: AutomationCommands[T]['dataType']): Promise<AutomationCommands[T]['returnType']> => {
debugVerbose('automation command \'%s\' requested with data: %O', message, data)
debug('BiDi middleware handling msg `%s` for top context %s', message, this.topLevelContextId)
switch (message) {
case 'key:press':
if (this.autContextId) {
@@ -1,13 +1,22 @@
import type Sinon from 'sinon'
import type { expect as Expect } from 'chai'
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')
const { expect, sinon }: { expect: typeof Expect, sinon: Sinon.SinonSandbox } = require('../../../spec_helper')
type ClientParams<T extends keyof WebdriverClient> = WebdriverClient[T] extends (...args: any[]) => any ?
Parameters<WebdriverClient[T]> :
never
type ClientReturn<T extends keyof WebdriverClient> = WebdriverClient[T] extends (...args: any[]) => any ?
ReturnType<WebdriverClient[T]> :
never
describe('key:press automation command', () => {
describe('cdp()', () => {
describe('cdp', () => {
let sendFn: Sinon.SinonStub<Parameters<SendDebuggerCommand>, ReturnType<SendDebuggerCommand>>
const topFrameId = 'abc'
const autFrameId = 'def'
@@ -178,16 +187,20 @@ describe('key:press automation command', () => {
const otherElement = {
'element-6066-11e4-a52e-4f735466cecf': 'uuid-2',
}
const topLevelContext = 'b7173d71-c76c-41ec-beff-25a72f7cae13'
beforeEach(() => {
// can't create a sinon stubbed instance because webdriver doesn't export the constructor. Because it's known that
// bidiKeypress only invokes inputPerformActions, and inputPerformActions is properly typed, this is okay.
// @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']>>(),
inputPerformActions: sinon.stub<ClientParams<'inputPerformActions'>, ClientReturn<'inputPerformActions'>>(),
getActiveElement: sinon.stub<ClientParams<'getActiveElement'>, ClientReturn<'getActiveElement'>>(),
findElement: sinon.stub<ClientParams<'findElement'>, ClientReturn<'findElement'>>(),
scriptEvaluate: sinon.stub<ClientParams<'scriptEvaluate'>, ClientReturn<'scriptEvaluate'>>(),
getWindowHandle: sinon.stub<ClientParams<'getWindowHandle'>, ClientReturn<'getWindowHandle'>>(),
switchToWindow: sinon.stub<ClientParams<'switchToWindow'>, ClientReturn<'switchToWindow'>>().resolves(),
browsingContextGetTree: sinon.stub<ClientParams<'browsingContextGetTree'>, ClientReturn<'browsingContextGetTree'>>(),
}
autContext = 'someContextId'
@@ -195,10 +208,21 @@ describe('key:press automation command', () => {
key = 'Tab'
client.inputPerformActions.resolves()
client.browsingContextGetTree.resolves({
contexts: [
{
context: topLevelContext,
children: [],
url: 'someUrl',
userContext: 'userContext',
},
],
})
})
describe('when the aut iframe is not in focus', () => {
beforeEach(() => {
client.getWindowHandle.resolves(topLevelContext)
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)
@@ -226,7 +250,41 @@ describe('key:press automation command', () => {
})
})
describe('when webdriver classic has no active window', () => {
beforeEach(() => {
client.getWindowHandle.rejects(new Error())
})
it('activates the top level context window', async () => {
await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix')
expect(client.switchToWindow).to.have.been.calledWith(topLevelContext)
})
})
describe('when webdriver classic has the top level context as the active window', () => {
beforeEach(() => {
client.getWindowHandle.resolves(topLevelContext)
})
it('does not activate the top level context window', async () => {
await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix')
expect(client.switchToWindow).not.to.have.been.called
})
})
describe('when webdriver classic has a different window than the top level context as the active window', () => {
beforeEach(() => {
client.getWindowHandle.resolves('fa54442b-bc42-45fa-9996-88b7fd066211')
})
it('activates the top level context window', async () => {
await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix')
expect(client.switchToWindow).to.have.been.calledWith(topLevelContext)
})
})
it('calls client.inputPerformActions with a keydown and keyup action', async () => {
client.getWindowHandle.resolves(topLevelContext)
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)
@@ -0,0 +1,8 @@
module.exports = {
e2e: {
supportFile: false,
setupNodeEvents (on, config) {
// implement node event listeners here
},
},
}
@@ -0,0 +1,5 @@
describe('this one should pass', () => {
it('dispatches a key press', () => {
cy.press(Cypress.Keyboard.Keys.TAB)
})
})
@@ -0,0 +1,5 @@
describe('it should also pass', () => {
it('dispatches a key press', () => {
cy.press(Cypress.Keyboard.Keys.TAB)
})
})
+12
View File
@@ -0,0 +1,12 @@
import systemTests from '../lib/system-tests'
describe('e2e issue 31466: cy.press only works in the first spec in firefox', () => {
systemTests.setup()
systemTests.it('does not error when dispatching cy.press', {
spec: 'first_spec.cy.js,second_spec.cy.js',
project: 'cy-press-second-spec-error',
expectedExitCode: 0,
browser: 'firefox',
})
})