From 698b6086af4cbe457611f31f9f76dae23346d430 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 29 May 2025 16:35:56 -0500 Subject: [PATCH 01/19] chore: add promptBackend as an additional Cypress-attached function --- cli/types/cypress.d.ts | 10 ----- packages/app/src/runner/event-manager.ts | 22 +++++++---- .../driver/cypress/e2e/cypress/cypress.cy.js | 37 ++++++++++++++++++ .../driver/src/cross-origin/events/socket.ts | 1 + packages/driver/src/cypress.ts | 12 +++++- .../driver/types/internal-types-lite.d.ts | 1 + packages/server/lib/socket-base.ts | 38 ++++++++++++++----- packages/server/test/unit/socket_spec.js | 12 +++--- 8 files changed, 98 insertions(+), 35 deletions(-) diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 6611f3ec08ef..65f51f96722e 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -1826,11 +1826,6 @@ declare namespace Cypress { */ prevUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> - /** - * TODO: add docs - */ - prompt(message: string, options?: Partial): Chainable - /** * Read a file and yield its contents. * @@ -3163,11 +3158,6 @@ declare namespace Cypress { * @default false */ experimentalStudio: boolean - /** - * Enables the prompt command feature. - * @default false - */ - experimentalPromptCommand: boolean /** * Adds support for testing in the WebKit browser engine used by Safari. See https://on.cypress.io/webkit-experiment for more information. * @default false diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index b5c3326924c6..bec112889412 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -41,7 +41,7 @@ interface AddGlobalListenerOptions { } const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ') -const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame dev-server:on-spec-update'.split(' ') +const driverToSocketEvents = 'backend:request prompt:backend:request automation:request mocha recorder:frame dev-server:on-spec-update'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies dev-server:on-spec-updated'.split(' ') @@ -797,11 +797,7 @@ export class EventManager { }, ) - /** - * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. - * Return it's response. - */ - Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => { + const baseBackendRequestHandler = async ({ args }, { source, responseEvent }) => { let response try { @@ -811,7 +807,19 @@ export class EventManager { } Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response) - }) + } + + /** + * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. + * Return it's response. + */ + Cypress.primaryOriginCommunicator.on('backend:request', baseBackendRequestHandler) + + /** + * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. + * Return it's response. + */ + Cypress.primaryOriginCommunicator.on('prompt:backend:request', baseBackendRequestHandler) /** * Call an automation request for the requesting spec bridge since we cannot have websockets in the spec bridges. diff --git a/packages/driver/cypress/e2e/cypress/cypress.cy.js b/packages/driver/cypress/e2e/cypress/cypress.cy.js index 341adfbfd686..0dc934fb7b01 100644 --- a/packages/driver/cypress/e2e/cypress/cypress.cy.js +++ b/packages/driver/cypress/e2e/cypress/cypress.cy.js @@ -86,6 +86,43 @@ describe('driver/src/cypress/index', () => { }) }) + context('#promptBackend', () => { + it('sets __stackCleaned__ on errors', function () { + cy.stub(CypressInstance, 'emit') + .withArgs('prompt:backend:request') + .yieldsAsync({ + error: { + name: 'Error', + message: 'msg', + stack: 'stack', + }, + }) + + return CypressInstance.promptBackend('foo') + .catch((err) => { + expect(err.backend).to.be.true + + expect(err.stack).not.to.include('From previous event') + }) + }) + + // https://github.com/cypress-io/cypress/issues/4346 + it('can complete if a circular reference is sent', () => { + const foo = { + bar: {}, + } + + foo.bar.baz = foo + + return Cypress.promptBackend('foo', foo) + .then(() => { + throw new Error('should not reach') + }).catch((e) => { + expect(e.message).to.eq('You requested a backend event we cannot handle: foo') + }) + }) + }) + context('.isCy', () => { it('returns true on cy, cy chainable', () => { expect(Cypress.isCy(cy)).to.be.true diff --git a/packages/driver/src/cross-origin/events/socket.ts b/packages/driver/src/cross-origin/events/socket.ts index a159e1ea5554..754d88bc59b4 100644 --- a/packages/driver/src/cross-origin/events/socket.ts +++ b/packages/driver/src/cross-origin/events/socket.ts @@ -16,5 +16,6 @@ export const handleSocketEvents = (Cypress) => { } Cypress.on('backend:request', (...args) => onRequest('backend:request', args)) + Cypress.on('prompt:backend:request', (...args) => onRequest('prompt:backend:request', args)) Cypress.on('automation:request', (...args) => onRequest('automation:request', args)) } diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index c8657904fae7..c5db1e2d5205 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -764,7 +764,7 @@ class $Cypress { } } - backend (eventName, ...args) { + private baseBackendRequestHandler (emitEventName: string, eventName, ...args) { return new Promise((resolve, reject) => { const fn = function (reply) { const e = reply.error @@ -787,10 +787,18 @@ class $Cypress { return resolve(reply.response) } - return this.emit('backend:request', eventName, ...args, fn) + return this.emit(emitEventName, eventName, ...args, fn) }) } + backend (eventName, ...args) { + return this.baseBackendRequestHandler('backend:request', eventName, ...args) + } + + promptBackend (eventName, ...args) { + return this.baseBackendRequestHandler('prompt:backend:request', eventName, ...args) + } + automation (eventName, ...args) { // wrap action in promise return new Promise((resolve, reject) => { diff --git a/packages/driver/types/internal-types-lite.d.ts b/packages/driver/types/internal-types-lite.d.ts index c41aef97cf6a..1bb432360045 100644 --- a/packages/driver/types/internal-types-lite.d.ts +++ b/packages/driver/types/internal-types-lite.d.ts @@ -19,6 +19,7 @@ declare namespace Cypress { (action: 'net:stubbing:event', frame: any) (action: 'request:event', data: any) (action: 'backend:request', fn: (...any) => void) + (action: 'prompt:backend:request', fn: (...any) => void) (action: 'automation:request', fn: (...any) => void) (action: 'viewport:changed', fn?: (viewport: { viewportWidth: string, viewportHeight: string }, callback: () => void) => void) (action: 'before:screenshot', fn: (config: {}, fn: () => void) => void) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index e99e18f5d2bd..0bfdd0283819 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -454,6 +454,34 @@ export class SocketBase { cyPrompt = cp }) + socket.on('prompt:backend:request', (eventName: string, ...args) => { + // cb is always the last argument + const cb = args.pop() + + debug('prompt:backend:request %o', { eventName, args }) + + const promptBackendRequest = () => { + switch (eventName) { + case 'wait:for:cy:prompt:ready': + return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { + return { + success: cyPrompt && cyPrompt.status === 'INITIALIZED', + } + }) + default: { + return cyPrompt?.handleBackendRequest(eventName, ...args) + } + } + } + + return Bluebird.try(promptBackendRequest) + .then((resp) => { + return cb({ response: resp }) + }).catch((err) => { + return cb({ error: errors.cloneErr(err) }) + }) + }) + socket.on('backend:request', (eventName: string, ...args) => { const userAgent = socket.request?.headers['user-agent'] || getCtx().coreData.app.browserUserAgent @@ -463,10 +491,6 @@ export class SocketBase { debug('backend:request %o', { eventName, args }) const backendRequest = () => { - if (eventName.startsWith('cy:prompt:')) { - return cyPrompt?.handleBackendRequest(eventName, ...args) - } - switch (eventName) { case 'preserve:run:state': runState = args[0] @@ -544,12 +568,6 @@ export class SocketBase { }) case 'close:extra:targets': return options.closeExtraTargets() - case 'wait:for:cy:prompt:ready': - return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { - return { - success: cyPrompt && cyPrompt.status === 'INITIALIZED', - } - }) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) } diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index e22b260b9774..6179e4f738ad 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -547,7 +547,7 @@ describe('lib/socket', () => { }) }) - context('on(backend:request, wait:for:cy:prompt:ready)', () => { + context('on(prompt:backend:request, wait:for:cy:prompt:ready)', () => { it('awaits cy prompt ready and returns true if cy prompt is ready', function (done) { const mockCyPrompt = { status: 'INITIALIZED', @@ -555,7 +555,7 @@ describe('lib/socket', () => { ctx.coreData.cyPromptLifecycleManager.getCyPrompt.resolves(mockCyPrompt) - return this.client.emit('backend:request', 'wait:for:cy:prompt:ready', (resp) => { + return this.client.emit('prompt:backend:request', 'wait:for:cy:prompt:ready', (resp) => { expect(resp.response).to.deep.eq({ success: true }) return done() @@ -569,7 +569,7 @@ describe('lib/socket', () => { ctx.coreData.cyPromptLifecycleManager.getCyPrompt.resolves(mockCyPrompt) - return this.client.emit('backend:request', 'wait:for:cy:prompt:ready', (resp) => { + return this.client.emit('prompt:backend:request', 'wait:for:cy:prompt:ready', (resp) => { expect(resp.response).to.deep.eq({ success: false }) return done() @@ -577,7 +577,7 @@ describe('lib/socket', () => { }) }) - context('on(backend:request, cy:prompt)', () => { + context('on(prompt:backend:request, default)', () => { it('calls handleBackendRequest with the correct arguments', function (done) { // Verify that registerCyPromptReadyListener was called expect(ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener).to.be.called @@ -592,9 +592,9 @@ describe('lib/socket', () => { registerCyPromptReadyListenerCallback(mockCyPrompt) - return this.client.emit('backend:request', 'cy:prompt:init', 'foo', (resp) => { + return this.client.emit('prompt:backend:request', 'prompt:init', 'foo', (resp) => { expect(resp.response).to.deep.eq({ foo: 'bar' }) - expect(mockCyPrompt.handleBackendRequest).to.be.calledWith('cy:prompt:init', 'foo') + expect(mockCyPrompt.handleBackendRequest).to.be.calledWith('prompt:init', 'foo') return done() }) From 308fa25a26b4d012e45009f5be29ab9fcf9abfaf Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 29 May 2025 21:24:15 -0500 Subject: [PATCH 02/19] Update packages/app/src/runner/event-manager.ts --- packages/app/src/runner/event-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index bec112889412..e252889d3660 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -816,7 +816,7 @@ export class EventManager { Cypress.primaryOriginCommunicator.on('backend:request', baseBackendRequestHandler) /** - * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. + * Call a prompt backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. * Return it's response. */ Cypress.primaryOriginCommunicator.on('prompt:backend:request', baseBackendRequestHandler) From 085754ec839c27f50a72ee9753b0f4f853f34211 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 29 May 2025 21:25:51 -0500 Subject: [PATCH 03/19] update types --- packages/driver/src/cy/commands/prompt/prompt-driver-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts index 3f742f4b9678..9309ac634b1a 100644 --- a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts +++ b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts @@ -1,5 +1,5 @@ export interface CypressInternal extends Cypress.Cypress { - backend: (eventName: string, ...args: any[]) => Promise + promptBackend: (eventName: string, ...args: any[]) => Promise } export interface CyPromptDriverDefaultShape { From f1296d343b29dc954e701987c3f6bb32f7b9c1bc Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 29 May 2025 21:43:01 -0500 Subject: [PATCH 04/19] fix types --- packages/driver/cypress.config.ts | 1 + packages/driver/cypress/e2e/commands/prompt.cy.ts | 1 + packages/server/lib/project-base.ts | 1 + packages/types/src/config.ts | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress.config.ts b/packages/driver/cypress.config.ts index d7df5cbee75b..f67b3467effa 100644 --- a/packages/driver/cypress.config.ts +++ b/packages/driver/cypress.config.ts @@ -7,6 +7,7 @@ export const baseConfig: Cypress.ConfigOptions = { experimentalStudio: true, experimentalMemoryManagement: true, experimentalWebKitSupport: true, + // @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag experimentalPromptCommand: true, hosts: { 'foobar.com': '127.0.0.1', diff --git a/packages/driver/cypress/e2e/commands/prompt.cy.ts b/packages/driver/cypress/e2e/commands/prompt.cy.ts index 9fb327349909..580c330bef54 100644 --- a/packages/driver/cypress/e2e/commands/prompt.cy.ts +++ b/packages/driver/cypress/e2e/commands/prompt.cy.ts @@ -4,6 +4,7 @@ describe('src/cy/commands/prompt', () => { // TODO: add more tests when cy.prompt is built out, but for now this just // verifies that the command executes without throwing an error + // @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag cy.prompt('Hello, world!') }) }) diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 42d0986d6477..e0b2f7f7dd79 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -157,6 +157,7 @@ export class ProjectBase extends EE { process.chdir(this.projectRoot) this._server = new ServerBase(cfg) + // @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag if (cfg.projectId && cfg.experimentalPromptCommand) { const cyPromptLifecycleManager = new CyPromptLifecycleManager() diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 84b11e45aed9..0c5018bbb3d5 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -30,7 +30,7 @@ export interface FullConfig extends Partial - & Pick // TODO: Figure out how to type this better. + & Pick // TODO: Figure out how to type this better. export interface SettingsOptions { testingType?: 'component' |'e2e' From 81de3557db4efe7387514bf0a2fa2bde800bfe6f Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Thu, 29 May 2025 22:07:17 -0500 Subject: [PATCH 05/19] fix spacing --- packages/driver/src/cy/commands/prompt/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 211cfc4f1c23..0be9225c9cf0 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -1,5 +1,5 @@ import { init, loadRemote } from '@module-federation/runtime' -import type{ CyPromptDriverDefaultShape } from './prompt-driver-types' +import type { CyPromptDriverDefaultShape } from './prompt-driver-types' interface CyPromptDriver { default: CyPromptDriverDefaultShape } From e7365c3391c9046fdd777e563376863ed61ccaa0 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 15:35:26 -0500 Subject: [PATCH 06/19] refactor --- packages/app/src/runner/event-manager.ts | 22 +++------ .../driver/cypress/e2e/cypress/cypress.cy.js | 37 -------------- .../driver/src/cy/commands/prompt/index.ts | 46 ++++++++++++------ .../cy/commands/prompt/prompt-driver-types.ts | 20 +++++++- packages/driver/src/cypress.ts | 10 ++-- .../driver/types/internal-types-lite.d.ts | 2 +- .../lib/cloud/cy-prompt/CyPromptManager.ts | 6 ++- packages/server/lib/socket-base.ts | 46 +++++------------- .../cloud/cy-prompt/test-cy-prompt.ts | 5 +- .../cloud/cy-prompt/CyPromptManager_spec.ts | 35 +++----------- packages/server/test/unit/socket_spec.js | 48 ++++++++----------- .../src/cy-prompt/cy-prompt-server-types.ts | 7 +-- 12 files changed, 107 insertions(+), 177 deletions(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index e252889d3660..b5c3326924c6 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -41,7 +41,7 @@ interface AddGlobalListenerOptions { } const driverToLocalAndReporterEvents = 'run:start run:end'.split(' ') -const driverToSocketEvents = 'backend:request prompt:backend:request automation:request mocha recorder:frame dev-server:on-spec-update'.split(' ') +const driverToSocketEvents = 'backend:request automation:request mocha recorder:frame dev-server:on-spec-update'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:cookies dev-server:on-spec-updated'.split(' ') @@ -797,7 +797,11 @@ export class EventManager { }, ) - const baseBackendRequestHandler = async ({ args }, { source, responseEvent }) => { + /** + * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. + * Return it's response. + */ + Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => { let response try { @@ -807,19 +811,7 @@ export class EventManager { } Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response) - } - - /** - * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. - * Return it's response. - */ - Cypress.primaryOriginCommunicator.on('backend:request', baseBackendRequestHandler) - - /** - * Call a prompt backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. - * Return it's response. - */ - Cypress.primaryOriginCommunicator.on('prompt:backend:request', baseBackendRequestHandler) + }) /** * Call an automation request for the requesting spec bridge since we cannot have websockets in the spec bridges. diff --git a/packages/driver/cypress/e2e/cypress/cypress.cy.js b/packages/driver/cypress/e2e/cypress/cypress.cy.js index 0dc934fb7b01..341adfbfd686 100644 --- a/packages/driver/cypress/e2e/cypress/cypress.cy.js +++ b/packages/driver/cypress/e2e/cypress/cypress.cy.js @@ -86,43 +86,6 @@ describe('driver/src/cypress/index', () => { }) }) - context('#promptBackend', () => { - it('sets __stackCleaned__ on errors', function () { - cy.stub(CypressInstance, 'emit') - .withArgs('prompt:backend:request') - .yieldsAsync({ - error: { - name: 'Error', - message: 'msg', - stack: 'stack', - }, - }) - - return CypressInstance.promptBackend('foo') - .catch((err) => { - expect(err.backend).to.be.true - - expect(err.stack).not.to.include('From previous event') - }) - }) - - // https://github.com/cypress-io/cypress/issues/4346 - it('can complete if a circular reference is sent', () => { - const foo = { - bar: {}, - } - - foo.bar.baz = foo - - return Cypress.promptBackend('foo', foo) - .then(() => { - throw new Error('should not reach') - }).catch((e) => { - expect(e.message).to.eq('You requested a backend event we cannot handle: foo') - }) - }) - }) - context('.isCy', () => { it('returns true on cy, cy chainable', () => { expect(Cypress.isCy(cy)).to.be.true diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 0be9225c9cf0..959b8b16255f 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -1,10 +1,19 @@ import { init, loadRemote } from '@module-federation/runtime' import type { CyPromptDriverDefaultShape } from './prompt-driver-types' +import type Emitter from 'component-emitter' interface CyPromptDriver { default: CyPromptDriverDefaultShape } -let initializedCyPrompt: CyPromptDriverDefaultShape | null = null -const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise => { +declare global { + interface Window { + getEventManager: () => { + ws: Emitter + } + } +} + +let initializedModule: CyPromptDriverDefaultShape | null = null +const initializeModule = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['cy']): Promise => { // Wait for the cy prompt bundle to be downloaded and ready const { success } = await Cypress.backend('wait:for:cy:prompt:ready') @@ -34,26 +43,35 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise { + let cloudModule = initializedModule + + if (!cloudModule) { + cloudModule = await initializeModule(Cypress, cy) + } + + return cloudModule.createCyPrompt({ + Cypress, + cy, + eventManager: window.getEventManager(), + }) } export default (Commands, Cypress, cy) => { if (Cypress.config('experimentalPromptCommand')) { + const initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy) + Commands.addAll({ - async prompt (message: string) { + async prompt (message: string, options: object = {}) { try { - let cloud = initializedCyPrompt - - // If the cy prompt driver is not initialized, - // we need to wait for it to be initialized - // before using it - if (!cloud) { - cloud = await initializeCloudCyPrompt(Cypress) - } + const cyPrompt = await initializeCloudCyPromptPromise - return await cloud.cyPrompt(Cypress, message) + return await cyPrompt(message, options) } catch (error) { // TODO: handle this better throw new Error(`CyPromptDriver not found: ${error}`) diff --git a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts index 9309ac634b1a..ddcbd07d20c0 100644 --- a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts +++ b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts @@ -1,7 +1,23 @@ +import type Emitter from 'component-emitter' + export interface CypressInternal extends Cypress.Cypress { - promptBackend: (eventName: string, ...args: any[]) => Promise + backendRequestHandler: (backendRequestNamespace: string, emitter: Emitter, eventName: string, ...args: any[]) => Promise + primaryOriginCommunicator: import('eventemitter2').EventEmitter2 & { + toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void + userInvocationStack?: string + } +} + +export interface CyPromptEventManager { + ws: Emitter +} + +export interface CyPromptOptions { + Cypress: CypressInternal + cy: Cypress.cy + eventManager: CyPromptEventManager } export interface CyPromptDriverDefaultShape { - cyPrompt: (Cypress: CypressInternal, text: string) => Promise + createCyPrompt: (options: CyPromptOptions) => (text: string, commandOptions?: object) => Promise } diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index c5db1e2d5205..dd2cc1c439b9 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -764,7 +764,7 @@ class $Cypress { } } - private baseBackendRequestHandler (emitEventName: string, eventName, ...args) { + backendRequestHandler (backendRequestNamespace: string, emitter, eventName, ...args) { return new Promise((resolve, reject) => { const fn = function (reply) { const e = reply.error @@ -787,16 +787,12 @@ class $Cypress { return resolve(reply.response) } - return this.emit(emitEventName, eventName, ...args, fn) + return emitter.emit(backendRequestNamespace, eventName, ...args, fn) }) } backend (eventName, ...args) { - return this.baseBackendRequestHandler('backend:request', eventName, ...args) - } - - promptBackend (eventName, ...args) { - return this.baseBackendRequestHandler('prompt:backend:request', eventName, ...args) + return this.backendRequestHandler('backend:request', this, eventName, ...args) } automation (eventName, ...args) { diff --git a/packages/driver/types/internal-types-lite.d.ts b/packages/driver/types/internal-types-lite.d.ts index 1bb432360045..cf535337755f 100644 --- a/packages/driver/types/internal-types-lite.d.ts +++ b/packages/driver/types/internal-types-lite.d.ts @@ -12,6 +12,7 @@ declare namespace Cypress { toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void userInvocationStack?: string } + backendRequestHandler: (backendRequestNamespace: string, emitter: Emitter, eventName: string, ...args: any[]) => Promise } interface Actions { @@ -19,7 +20,6 @@ declare namespace Cypress { (action: 'net:stubbing:event', frame: any) (action: 'request:event', data: any) (action: 'backend:request', fn: (...any) => void) - (action: 'prompt:backend:request', fn: (...any) => void) (action: 'automation:request', fn: (...any) => void) (action: 'viewport:changed', fn?: (viewport: { viewportWidth: string, viewportHeight: string }, callback: () => void) => void) (action: 'before:screenshot', fn: (config: {}, fn: () => void) => void) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts index ce247d5a156f..36eb4f1feb4e 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts @@ -2,6 +2,7 @@ import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' +import { Socket } from 'socket.io' interface CyPromptServer { default: CyPromptServerDefaultShape } @@ -38,9 +39,9 @@ export class CyPromptManager implements CyPromptManagerShape { } } - async handleBackendRequest (eventName: string, ...args: any[]): Promise { + addSocketListeners (socket: Socket): void { if (this._cyPromptServer) { - return this.invokeAsync('handleBackendRequest', { isEssential: true }, eventName, ...args) + this.invokeSync('addSocketListeners', { isEssential: true }, socket) } } @@ -54,6 +55,7 @@ export class CyPromptManager implements CyPromptManagerShape { } try { + // @ts-expect-error - TS not associating the method & args properly, even though we know it's correct return this._cyPromptServer[method].apply(this._cyPromptServer, args) } catch (error: unknown) { let actualError: Error diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 0bfdd0283819..7a4dd737b0d7 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -23,7 +23,7 @@ import type { Automation } from './automation' // eslint-disable-next-line no-duplicate-imports import type { Socket } from '@packages/socket' -import type { RunState, CachedTestState, ProtocolManagerShape, AutomationCommands, CyPromptManagerShape } from '@packages/types' +import type { RunState, CachedTestState, ProtocolManagerShape, AutomationCommands } from '@packages/types' import memory from './browsers/memory' import { privilegedCommandsManager } from './privileged-commands/privileged-commands-manager' @@ -411,6 +411,10 @@ export class SocketBase { studio.addSocketListeners(socket) }) + getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cyPrompt) => { + cyPrompt.addSocketListeners(socket) + }) + socket.on('studio:init', async (cb) => { try { const { canAccessStudioAI } = await options.onStudioInit() @@ -448,40 +452,6 @@ export class SocketBase { } }) - let cyPrompt: CyPromptManagerShape | undefined - - getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cp) => { - cyPrompt = cp - }) - - socket.on('prompt:backend:request', (eventName: string, ...args) => { - // cb is always the last argument - const cb = args.pop() - - debug('prompt:backend:request %o', { eventName, args }) - - const promptBackendRequest = () => { - switch (eventName) { - case 'wait:for:cy:prompt:ready': - return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { - return { - success: cyPrompt && cyPrompt.status === 'INITIALIZED', - } - }) - default: { - return cyPrompt?.handleBackendRequest(eventName, ...args) - } - } - } - - return Bluebird.try(promptBackendRequest) - .then((resp) => { - return cb({ response: resp }) - }).catch((err) => { - return cb({ error: errors.cloneErr(err) }) - }) - }) - socket.on('backend:request', (eventName: string, ...args) => { const userAgent = socket.request?.headers['user-agent'] || getCtx().coreData.app.browserUserAgent @@ -568,6 +538,12 @@ export class SocketBase { }) case 'close:extra:targets': return options.closeExtraTargets() + case 'wait:for:cy:prompt:ready': + return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { + return { + success: cyPrompt && cyPrompt.status === 'INITIALIZED', + } + }) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) } diff --git a/packages/server/test/support/fixtures/cloud/cy-prompt/test-cy-prompt.ts b/packages/server/test/support/fixtures/cloud/cy-prompt/test-cy-prompt.ts index 513a7e6107a6..34b27e9b3d1d 100644 --- a/packages/server/test/support/fixtures/cloud/cy-prompt/test-cy-prompt.ts +++ b/packages/server/test/support/fixtures/cloud/cy-prompt/test-cy-prompt.ts @@ -2,14 +2,15 @@ import type { CyPromptServerShape, CyPromptServerDefaultShape } from '@packages/types' import type { Router } from 'express' +import type { Socket } from 'socket.io' class CyPromptServer implements CyPromptServerShape { initializeRoutes (router: Router): void { // This is a test implementation that does nothing } - handleBackendRequest (eventName: string, ...args: any[]): Promise { - return Promise.resolve() + addSocketListeners (socket: Socket): void { + // This is a test implementation that does nothing } } diff --git a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts index f57a98542cb9..cd0ab406561e 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts @@ -55,18 +55,6 @@ describe('lib/cloud/cy-prompt', () => { }) }) - describe('asynchronous method invocation', () => { - it('reports an error when a asynchronous method fails', async () => { - const error = new Error('foo') - - sinon.stub(cyPrompt, 'handleBackendRequest').throws(error) - - await cyPromptManager.handleBackendRequest('cy:prompt:start', {} as any) - - expect(cyPromptManager.status).to.eq('IN_ERROR') - }) - }) - describe('initializeRoutes', () => { it('initializes routes', () => { sinon.stub(cyPrompt, 'initializeRoutes') @@ -78,25 +66,14 @@ describe('lib/cloud/cy-prompt', () => { }) }) - describe('handleBackendRequest', () => { - it('calls handleBackendRequest on the cy prompt server', () => { - sinon.stub(cyPrompt, 'handleBackendRequest') - - cyPromptManager.handleBackendRequest('cy:prompt:start', {} as any) - - expect(cyPrompt.handleBackendRequest).to.be.calledWith('cy:prompt:start', {} as any) - }) - - it('does not call handleBackendRequest when cy prompt server is not defined', () => { - // Set _cyPromptServer to undefined - (cyPromptManager as any)._cyPromptServer = undefined - - // Create a spy on invokeSync to verify it's not called - const invokeSyncSpy = sinon.spy(cyPromptManager, 'invokeSync') + describe('addSocketListeners', () => { + it('adds socket listeners', () => { + sinon.stub(cyPrompt, 'addSocketListeners') + const mockSocket = sinon.stub() - cyPromptManager.handleBackendRequest('cy:prompt:start', {} as any) + cyPromptManager.addSocketListeners(mockSocket) - expect(invokeSyncSpy).to.not.be.called + expect(cyPrompt.addSocketListeners).to.be.calledWith(mockSocket) }) }) }) diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index 6179e4f738ad..5462874d4545 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -89,7 +89,7 @@ describe('lib/socket', () => { // Create a mock cy prompt object with handleBackendRequest method const mockCyPrompt = { - handleBackendRequest: sinon.stub().resolves({ foo: 'bar' }), + addSocketListeners: sinon.stub(), status: 'INITIALIZED', } @@ -547,7 +547,7 @@ describe('lib/socket', () => { }) }) - context('on(prompt:backend:request, wait:for:cy:prompt:ready)', () => { + context('on(backend:request, wait:for:cy:prompt:ready)', () => { it('awaits cy prompt ready and returns true if cy prompt is ready', function (done) { const mockCyPrompt = { status: 'INITIALIZED', @@ -555,7 +555,7 @@ describe('lib/socket', () => { ctx.coreData.cyPromptLifecycleManager.getCyPrompt.resolves(mockCyPrompt) - return this.client.emit('prompt:backend:request', 'wait:for:cy:prompt:ready', (resp) => { + return this.client.emit('backend:request', 'wait:for:cy:prompt:ready', (resp) => { expect(resp.response).to.deep.eq({ success: true }) return done() @@ -569,7 +569,7 @@ describe('lib/socket', () => { ctx.coreData.cyPromptLifecycleManager.getCyPrompt.resolves(mockCyPrompt) - return this.client.emit('prompt:backend:request', 'wait:for:cy:prompt:ready', (resp) => { + return this.client.emit('backend:request', 'wait:for:cy:prompt:ready', (resp) => { expect(resp.response).to.deep.eq({ success: false }) return done() @@ -577,30 +577,6 @@ describe('lib/socket', () => { }) }) - context('on(prompt:backend:request, default)', () => { - it('calls handleBackendRequest with the correct arguments', function (done) { - // Verify that registerCyPromptReadyListener was called - expect(ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener).to.be.called - - // Check that the callback was called with the mock cy prompt object - const registerCyPromptReadyListenerCallback = ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener.firstCall.args[0] - - expect(registerCyPromptReadyListenerCallback).to.be.a('function') - - // Verify the mock cy prompt's handleBackendRequest was called by the callback - const mockCyPrompt = { handleBackendRequest: sinon.stub().resolves({ foo: 'bar' }) } - - registerCyPromptReadyListenerCallback(mockCyPrompt) - - return this.client.emit('prompt:backend:request', 'prompt:init', 'foo', (resp) => { - expect(resp.response).to.deep.eq({ foo: 'bar' }) - expect(mockCyPrompt.handleBackendRequest).to.be.calledWith('prompt:init', 'foo') - - return done() - }) - }) - }) - context('on(save:app:state)', () => { it('calls onSavedStateChanged with the state', function (done) { return this.client.emit('save:app:state', { reporterWidth: 500 }, () => { @@ -684,6 +660,22 @@ describe('lib/socket', () => { }) }) + context('cy.prompt.addSocketListeners', () => { + it('calls addSocketListeners on cy prompt when socket connects', function () { + // Verify that registerCyPromptReadyListener was called + expect(ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener).to.be.called + + const registerCyPromptReadyListenerCallback = ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener.firstCall.args[0] + + expect(registerCyPromptReadyListenerCallback).to.be.a('function') + + const mockCyPrompt = { addSocketListeners: sinon.stub() } + + registerCyPromptReadyListenerCallback(mockCyPrompt) + expect(mockCyPrompt.addSocketListeners).to.be.called + }) + }) + context('#isRunnerSocketConnected', function () { it('returns false when runner is not connected', function () { expect(this.socket.isRunnerSocketConnected()).to.eq(false) diff --git a/packages/types/src/cy-prompt/cy-prompt-server-types.ts b/packages/types/src/cy-prompt/cy-prompt-server-types.ts index 8e6cdd83fb3b..b8de266025fb 100644 --- a/packages/types/src/cy-prompt/cy-prompt-server-types.ts +++ b/packages/types/src/cy-prompt/cy-prompt-server-types.ts @@ -1,11 +1,8 @@ -// Note: This file is owned by the cloud delivered -// cy prompt bundle. It is downloaded and copied here. -// It should not be modified directly here. - /// import type { Router } from 'express' import type { AxiosInstance } from 'axios' +import type { Socket } from 'socket.io' interface RetryOptions { maxAttempts: number @@ -36,7 +33,7 @@ export interface CyPromptServerOptions { export interface CyPromptServerShape { initializeRoutes(router: Router): void - handleBackendRequest: (eventName: string, ...args: any[]) => Promise + addSocketListeners(socket: Socket): void } export interface CyPromptServerDefaultShape { From 4020676db137842c8d341e17d545ed758a4d4c05 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 16:19:52 -0500 Subject: [PATCH 07/19] additional refactor --- .../driver/src/cy/commands/prompt/index.ts | 4 ++-- .../cy/commands/prompt/prompt-driver-types.ts | 19 +++++++++++++++++-- .../driver/types/internal-types-lite.d.ts | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 959b8b16255f..a14f177f0451 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -1,5 +1,5 @@ import { init, loadRemote } from '@module-federation/runtime' -import type { CyPromptDriverDefaultShape } from './prompt-driver-types' +import type { CypressInternal, CyPromptDriverDefaultShape } from './prompt-driver-types' import type Emitter from 'component-emitter' interface CyPromptDriver { default: CyPromptDriverDefaultShape } @@ -56,7 +56,7 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp } return cloudModule.createCyPrompt({ - Cypress, + Cypress: Cypress as CypressInternal, cy, eventManager: window.getEventManager(), }) diff --git a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts index ddcbd07d20c0..b32534e3d7e8 100644 --- a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts +++ b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts @@ -1,11 +1,24 @@ import type Emitter from 'component-emitter' +interface InternalActions extends Cypress.Actions { + ( + eventName: 'prompt:backend:request', + listener: (...args: any[]) => void + ): Cypress.Cypress +} + export interface CypressInternal extends Cypress.Cypress { - backendRequestHandler: (backendRequestNamespace: string, emitter: Emitter, eventName: string, ...args: any[]) => Promise + backendRequestHandler: ( + backendRequestNamespace: string, + eventName: string, + ...args: any[] + ) => Promise primaryOriginCommunicator: import('eventemitter2').EventEmitter2 & { toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void userInvocationStack?: string + toSource: (source: string, responseEvent: string, response: any) => void } + on: InternalActions } export interface CyPromptEventManager { @@ -19,5 +32,7 @@ export interface CyPromptOptions { } export interface CyPromptDriverDefaultShape { - createCyPrompt: (options: CyPromptOptions) => (text: string, commandOptions?: object) => Promise + createCyPrompt: ( + options: CyPromptOptions + ) => (text: string, commandOptions?: object) => Promise } diff --git a/packages/driver/types/internal-types-lite.d.ts b/packages/driver/types/internal-types-lite.d.ts index cf535337755f..0c1a809653ba 100644 --- a/packages/driver/types/internal-types-lite.d.ts +++ b/packages/driver/types/internal-types-lite.d.ts @@ -11,6 +11,7 @@ declare namespace Cypress { primaryOriginCommunicator: import('eventemitter2').EventEmitter2 & { toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void userInvocationStack?: string + toSource: (source: string, responseEvent: string, response: any) => void } backendRequestHandler: (backendRequestNamespace: string, emitter: Emitter, eventName: string, ...args: any[]) => Promise } From 633c14cd66e4c8d212b03db4353f2315361d1d79 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 17:54:38 -0500 Subject: [PATCH 08/19] fix type build --- packages/server/lib/cloud/cy-prompt/CyPromptManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts index 36eb4f1feb4e..325f6cd4324a 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts @@ -2,7 +2,7 @@ import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' -import { Socket } from 'socket.io' +import type { Socket } from 'socket.io' interface CyPromptServer { default: CyPromptServerDefaultShape } @@ -84,6 +84,7 @@ export class CyPromptManager implements CyPromptManagerShape { } try { + // @ts-expect-error - TS not associating the method & args properly, even though we know it's correct return await this._cyPromptServer[method].apply(this._cyPromptServer, args) } catch (error: unknown) { let actualError: Error From d3d6def8429839702cfed01afe5ebb49953e8447 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 18:22:29 -0500 Subject: [PATCH 09/19] fix build --- packages/driver/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/driver/package.json b/packages/driver/package.json index bf89f12c970a..a9508aa39d1d 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -53,6 +53,7 @@ "chai-subset": "1.6.0", "clone": "2.1.2", "common-tags": "1.8.0", + "component-emitter": "1.3.0", "compression": "1.8.0", "cookie-parser": "1.4.5", "core-js-pure": "3.21.0", From 4408bbf27f8d542fe2b6d753abef6704cbfb3e84 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sat, 31 May 2025 21:18:30 -0500 Subject: [PATCH 10/19] refactor --- packages/app/src/runner/event-manager.ts | 16 +------- .../driver/cypress/e2e/commands/prompt.cy.ts | 9 ++++- .../driver/src/cross-origin/communicator.ts | 2 +- packages/driver/src/cross-origin/cypress.ts | 9 ++++- .../driver/src/cross-origin/events/socket.ts | 37 ++++++++++--------- .../driver/src/cy/commands/prompt/index.ts | 4 +- .../cy/commands/prompt/prompt-driver-types.ts | 31 ++++++++++++---- packages/driver/src/cypress.ts | 32 ++++++++++++++-- packages/driver/types/internal-types.d.ts | 1 + 9 files changed, 93 insertions(+), 48 deletions(-) diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index b5c3326924c6..6d8231c13fbb 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -797,21 +797,7 @@ export class EventManager { }, ) - /** - * Call a backend request for the requesting spec bridge since we cannot have websockets in the spec bridges. - * Return it's response. - */ - Cypress.primaryOriginCommunicator.on('backend:request', async ({ args }, { source, responseEvent }) => { - let response - - try { - response = await Cypress.backend(...args) - } catch (error) { - response = { error } - } - - Cypress.primaryOriginCommunicator.toSource(source, responseEvent, response) - }) + Cypress.handlePrimaryOriginSocketEvent(Cypress, 'backend:request') /** * Call an automation request for the requesting spec bridge since we cannot have websockets in the spec bridges. diff --git a/packages/driver/cypress/e2e/commands/prompt.cy.ts b/packages/driver/cypress/e2e/commands/prompt.cy.ts index 580c330bef54..740e1cca69d1 100644 --- a/packages/driver/cypress/e2e/commands/prompt.cy.ts +++ b/packages/driver/cypress/e2e/commands/prompt.cy.ts @@ -1,10 +1,17 @@ describe('src/cy/commands/prompt', () => { it('executes the prompt command', () => { - cy.visit('/fixtures/dom.html') + cy.visit('http://www.foobar.com:3500/fixtures/dom.html') // TODO: add more tests when cy.prompt is built out, but for now this just // verifies that the command executes without throwing an error // @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag cy.prompt('Hello, world!') + + cy.visit('http://www.barbaz.com:3500/fixtures/dom.html') + + cy.origin('http://www.barbaz.com:3500', () => { + // @ts-expect-error - this will not error when we actually release the experimentalPromptCommand flag + cy.prompt('Hello, world!') + }) }) }) diff --git a/packages/driver/src/cross-origin/communicator.ts b/packages/driver/src/cross-origin/communicator.ts index 550a6955f79c..32564b8c2234 100644 --- a/packages/driver/src/cross-origin/communicator.ts +++ b/packages/driver/src/cross-origin/communicator.ts @@ -336,7 +336,7 @@ export class SpecBridgeCommunicator extends EventEmitter { }: { event: string data?: Cypress.ObjectLike - options: {syncGlobals: boolean} + options?: {syncGlobals: boolean} timeout: number }) { return new Promise((resolve, reject) => { diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index c6af33aad03c..64a0aca56c9f 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -14,7 +14,7 @@ import { bindToListeners } from '../cy/listeners' import { handleOriginFn } from './origin_fn' import { FINAL_SNAPSHOT_NAME } from '../cy/snapshots' import { handleLogs } from './events/logs' -import { handleSocketEvents } from './events/socket' +import { handleCrossOriginSocketEvent, handleDefaultCrossOriginSocketEvents } from './events/socket' import { handleSpecWindowEvents } from './events/spec_window' import { handleErrorEvent } from './events/errors' import { handleScreenshots } from './events/screenshots' @@ -120,6 +120,12 @@ const setup = ({ cypressConfig, env, isProtocolEnabled }: { cypressConfig: Cypre const { state, config } = Cypress + // These need to happen before the commands are created so that commands can + // use things like Cypress.backend during their creation. + handleDefaultCrossOriginSocketEvents(Cypress) + + Cypress.handleCrossOriginSocketEvent = handleCrossOriginSocketEvent + // @ts-ignore Cypress.Commands = $Commands.create(Cypress, cy, state, config) // @ts-ignore @@ -141,7 +147,6 @@ const setup = ({ cypressConfig, env, isProtocolEnabled }: { cypressConfig: Cypre handleOriginFn(Cypress, cy) handleLogs(Cypress) - handleSocketEvents(Cypress) handleSpecWindowEvents(cy) handleMiscEvents(Cypress, cy) handleScreenshots(Cypress) diff --git a/packages/driver/src/cross-origin/events/socket.ts b/packages/driver/src/cross-origin/events/socket.ts index 754d88bc59b4..3cc996a71e54 100644 --- a/packages/driver/src/cross-origin/events/socket.ts +++ b/packages/driver/src/cross-origin/events/socket.ts @@ -1,21 +1,24 @@ -export const handleSocketEvents = (Cypress) => { - const onRequest = async (event, args) => { - // The last argument is the callback, pop that off before messaging primary and call it with the response. - const callback = args.pop() - const response = await Cypress.specBridgeCommunicator.toPrimaryPromise({ - event, - data: { args }, - timeout: Cypress.config().defaultCommandTimeout, - }) +const onRequest = async (event, args) => { + // The last argument is the callback, pop that off before messaging primary and call it with the response. + const callback = args.pop() + const response = await Cypress.specBridgeCommunicator.toPrimaryPromise<{ error?: string, response?: any }>({ + event, + data: { args }, + timeout: Cypress.config().defaultCommandTimeout, + }) - if (response && response.error) { - return callback({ error: response.error }) - } - - callback({ response }) + if (response && response.error) { + return callback({ error: response.error }) } - Cypress.on('backend:request', (...args) => onRequest('backend:request', args)) - Cypress.on('prompt:backend:request', (...args) => onRequest('prompt:backend:request', args)) - Cypress.on('automation:request', (...args) => onRequest('automation:request', args)) + callback({ response }) +} + +export const handleCrossOriginSocketEvent = (Cypress, eventName: string) => { + Cypress.on(eventName, (...args) => onRequest(eventName, args)) +} + +export const handleDefaultCrossOriginSocketEvents = (Cypress) => { + handleCrossOriginSocketEvent(Cypress, 'backend:request') + handleCrossOriginSocketEvent(Cypress, 'automation:request') } diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index a14f177f0451..a56fe0ea9eb9 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -6,7 +6,7 @@ interface CyPromptDriver { default: CyPromptDriverDefaultShape } declare global { interface Window { - getEventManager: () => { + getEventManager?: () => { ws: Emitter } } @@ -58,7 +58,7 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp return cloudModule.createCyPrompt({ Cypress: Cypress as CypressInternal, cy, - eventManager: window.getEventManager(), + eventManager: window.getEventManager ? window.getEventManager() : undefined, }) } diff --git a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts index b32534e3d7e8..2b57f796ce6b 100644 --- a/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts +++ b/packages/driver/src/cy/commands/prompt/prompt-driver-types.ts @@ -7,20 +7,35 @@ interface InternalActions extends Cypress.Actions { ): Cypress.Cypress } -export interface CypressInternal extends Cypress.Cypress { +export interface CypressInternalBase extends Cypress.Cypress { backendRequestHandler: ( backendRequestNamespace: string, eventName: string, ...args: any[] ) => Promise - primaryOriginCommunicator: import('eventemitter2').EventEmitter2 & { - toSpecBridge: (origin: string, event: string, data?: any, responseEvent?: string) => void - userInvocationStack?: string - toSource: (source: string, responseEvent: string, response: any) => void - } on: InternalActions } +interface CrossOriginCypressInternal extends CypressInternalBase { + isCrossOriginSpecBridge: true + handleCrossOriginSocketEvent: ( + Cypress: CypressInternal, + eventName: string + ) => void +} + +interface SameOriginCypressInternal extends CypressInternalBase { + isCrossOriginSpecBridge: false + handlePrimaryOriginSocketEvent: ( + Cypress: CypressInternal, + eventName: string + ) => void +} + +export type CypressInternal = + | CrossOriginCypressInternal + | SameOriginCypressInternal + export interface CyPromptEventManager { ws: Emitter } @@ -28,7 +43,9 @@ export interface CyPromptEventManager { export interface CyPromptOptions { Cypress: CypressInternal cy: Cypress.cy - eventManager: CyPromptEventManager + // Note that the eventManager is present in same origin AUTs, but not cross origin + // so we need to check for it's presence before using it + eventManager?: CyPromptEventManager } export interface CyPromptDriverDefaultShape { diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index dd2cc1c439b9..9a74cd3ae786 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -88,6 +88,30 @@ interface AutomationError extends Error { // Are we running Cypress in Cypress? (Used for E2E Testing for Cypress in Cypress only) const isCypressInCypress = document.defaultView !== top +const handlePrimaryOriginSocketEvent = (Cypress, eventName: string) => { + Cypress.primaryOriginCommunicator.on( + eventName, + async ({ args }: { args: [string, any[]] }, { source, responseEvent }) => { + let response + + try { + response = await Cypress.backendRequestHandler( + eventName, + ...args, + ) + } catch (error) { + response = { error } + } + + Cypress.primaryOriginCommunicator.toSource( + source, + responseEvent, + response, + ) + }, + ) +} + class $Cypress { cy: any chai: any @@ -161,6 +185,8 @@ class $Cypress { sinon = sinon lolex = fakeTimers + handlePrimaryOriginSocketEvent = handlePrimaryOriginSocketEvent + static $: any static utils: any @@ -764,7 +790,7 @@ class $Cypress { } } - backendRequestHandler (backendRequestNamespace: string, emitter, eventName, ...args) { + backendRequestHandler (backendRequestNamespace: string, eventName, ...args) { return new Promise((resolve, reject) => { const fn = function (reply) { const e = reply.error @@ -787,12 +813,12 @@ class $Cypress { return resolve(reply.response) } - return emitter.emit(backendRequestNamespace, eventName, ...args, fn) + return Cypress.emit(backendRequestNamespace, eventName, ...args, fn) }) } backend (eventName, ...args) { - return this.backendRequestHandler('backend:request', this, eventName, ...args) + return this.backendRequestHandler('backend:request', eventName, ...args) } automation (eventName, ...args) { diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index bc632521e2b2..4794fca9cfbe 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -53,6 +53,7 @@ declare namespace Cypress { utils: CypressUtils events: Events specBridgeCommunicator: import('../src/cross-origin/communicator').SpecBridgeCommunicator + handleCrossOriginSocketEvent?: typeof handleCrossOriginSocketEvent mocha: $Mocha configure: (config: Cypress.ObjectLike) => void isCrossOriginSpecBridge: boolean From ac5407b80a8f4dde593de947483ffa4706d7600e Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 16:10:20 -0500 Subject: [PATCH 11/19] reword messages --- packages/driver/src/cy/commands/prompt/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index a56fe0ea9eb9..e24cfbc96b44 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -18,7 +18,7 @@ const initializeModule = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['c const { success } = await Cypress.backend('wait:for:cy:prompt:ready') if (!success) { - throw new Error('CyPromptDriver not found') + throw new Error('error waiting for cy prompt bundle to be downloaded and ready') } // Once the cy prompt bundle is downloaded and ready, @@ -40,7 +40,7 @@ const initializeModule = async (Cypress: Cypress.Cypress, cy: Cypress.Cypress['c const module = await loadRemote('cy-prompt') if (!module?.default) { - throw new Error('CyPromptDriver not found') + throw new Error('error loading cy prompt driver') } initializedModule = module.default From 629bd100b28920f3d90e993a6a7ae7e66c30a33d Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 16:29:38 -0500 Subject: [PATCH 12/19] fix --- .../server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts | 4 +++- packages/server/lib/socket-base.ts | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index e7bce3cd41cb..bdb773cf368d 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -43,7 +43,9 @@ export class CyPromptLifecycleManager { projectId, cloudDataSource, }).catch(async (error) => { - debug('Error during cy prompt manager setup: %o', error) + // TODO: remove this once we have a proper error reporting mechanism + // eslint-disable-next-line no-console + console.log('Error during cy prompt manager setup: %o', error) // const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production' // const cloudUrl = ctx.cloud.getCloudUrl(cloudEnv) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 7a4dd737b0d7..66145724c36a 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -543,6 +543,11 @@ export class SocketBase { return { success: cyPrompt && cyPrompt.status === 'INITIALIZED', } + }).catch((err) => { + return { + success: false, + error: err, + } }) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) From 80e0146024dfb57ccb0e29e6a522f505db51d101 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 16:44:12 -0500 Subject: [PATCH 13/19] debugging --- packages/server/lib/socket-base.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 66145724c36a..7c79f6a9baaa 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -540,10 +540,18 @@ export class SocketBase { return options.closeExtraTargets() case 'wait:for:cy:prompt:ready': return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { + // TODO: remove this once we have a proper error reporting mechanism + // eslint-disable-next-line no-console + console.log('cy prompt', cyPrompt) + return { success: cyPrompt && cyPrompt.status === 'INITIALIZED', } }).catch((err) => { + // TODO: remove this once we have a proper error reporting mechanism + // eslint-disable-next-line no-console + console.log('error getting cy prompt', err) + return { success: false, error: err, From b9de91d5e727859bb9c8e621a4a2fba57dfab413 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 18:39:15 -0500 Subject: [PATCH 14/19] undo debugging --- .../lib/cloud/cy-prompt/CyPromptLifecycleManager.ts | 4 +--- packages/server/lib/socket-base.ts | 13 ------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index bdb773cf368d..e7bce3cd41cb 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -43,9 +43,7 @@ export class CyPromptLifecycleManager { projectId, cloudDataSource, }).catch(async (error) => { - // TODO: remove this once we have a proper error reporting mechanism - // eslint-disable-next-line no-console - console.log('Error during cy prompt manager setup: %o', error) + debug('Error during cy prompt manager setup: %o', error) // const cloudEnv = (process.env.CYPRESS_CONFIG_ENV || process.env.CYPRESS_INTERNAL_ENV || 'production') as 'development' | 'staging' | 'production' // const cloudUrl = ctx.cloud.getCloudUrl(cloudEnv) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 7c79f6a9baaa..7a4dd737b0d7 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -540,22 +540,9 @@ export class SocketBase { return options.closeExtraTargets() case 'wait:for:cy:prompt:ready': return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { - // TODO: remove this once we have a proper error reporting mechanism - // eslint-disable-next-line no-console - console.log('cy prompt', cyPrompt) - return { success: cyPrompt && cyPrompt.status === 'INITIALIZED', } - }).catch((err) => { - // TODO: remove this once we have a proper error reporting mechanism - // eslint-disable-next-line no-console - console.log('error getting cy prompt', err) - - return { - success: false, - error: err, - } }) default: throw new Error(`You requested a backend event we cannot handle: ${eventName}`) From 342dd99b16ec017e56d151ed7b7402dd3337288b Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 11:34:46 -0500 Subject: [PATCH 15/19] PR comment --- packages/driver/src/cypress.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 9a74cd3ae786..b0c5380c3fd6 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -88,15 +88,15 @@ interface AutomationError extends Error { // Are we running Cypress in Cypress? (Used for E2E Testing for Cypress in Cypress only) const isCypressInCypress = document.defaultView !== top -const handlePrimaryOriginSocketEvent = (Cypress, eventName: string) => { +const handlePrimaryOriginSocketEvent = (Cypress, backendRequestNamespace: string) => { Cypress.primaryOriginCommunicator.on( - eventName, + backendRequestNamespace, async ({ args }: { args: [string, any[]] }, { source, responseEvent }) => { let response try { response = await Cypress.backendRequestHandler( - eventName, + backendRequestNamespace, ...args, ) } catch (error) { From d1a1186d804775550be70131d9b42906d04e1dc4 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 12:17:58 -0500 Subject: [PATCH 16/19] fix tests --- .../cypress/e2e/e2e/origin/patches.cy.ts | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts index 2bdc0fbdb6b3..f1d62636dc91 100644 --- a/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/patches.cy.ts @@ -63,7 +63,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout describe('from the AUT', () => { beforeEach(() => { cy.intercept('/test-request').as('testRequest') - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="xhr-fetch-requests"]').click() @@ -87,7 +87,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout cy.wait('@testRequest') cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -107,7 +107,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout cy.wait('@testRequest') cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -127,7 +127,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout cy.wait('@testRequest') cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -152,7 +152,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout ) cy.then(() => { - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) }) @@ -171,7 +171,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout ) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://app.foobar.com:3500/test-request', resourceType: 'fetch', credentialStatus: 'include', @@ -184,7 +184,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout describe('from the spec bridge', () => { beforeEach(() => { cy.intercept('/test-request').as('testRequest') - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="xhr-fetch-requests"]').click() @@ -216,7 +216,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request-credentials', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -246,7 +246,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request-credentials', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -274,7 +274,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request-credentials', resourceType: 'fetch', credentialStatus: assertCredentialStatus, @@ -297,7 +297,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) }) }) @@ -326,7 +326,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://app.foobar.com:3500/test-request', resourceType: 'fetch', credentialStatus: 'include', @@ -339,13 +339,13 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout // manually remove the spec bridge iframe to ensure Cypress.state('window') is not already set window.top?.document.getElementById('Spec\ Bridge:\ foobar.com')?.remove() - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="xhr-fetch-requests-onload"]').click() cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://localhost:3500/foo.bar.baz.json', resourceType: 'fetch', credentialStatus: 'same-origin', @@ -354,7 +354,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) it('does not patch fetch in the spec window or the AUT if the AUT is on the primary', () => { - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('fixtures/xhr-fetch-requests.html') cy.window().then((win) => { @@ -371,14 +371,14 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout cy.then(async () => { await fetch('/test-request') - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('request:sent:with:credentials') }) // expect AUT to NOT be patched in primary cy.window().then(async (win) => { await win.fetch('/test-request') - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('request:sent:with:credentials') }) }) }) @@ -387,7 +387,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout describe('from the AUT', () => { beforeEach(() => { cy.intercept('/test-request').as('testRequest') - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="xhr-fetch-requests"]').click() @@ -409,7 +409,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout cy.wait('@testRequest') cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request', resourceType: 'xhr', credentialStatus: withCredentials, @@ -431,7 +431,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) }) }) @@ -449,7 +449,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://app.foobar.com:3500/test-request', resourceType: 'xhr', credentialStatus: true, @@ -461,9 +461,9 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout describe('from the spec bridge', () => { beforeEach(() => { cy.intercept('/test-request').as('testRequest') - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.origin('http://www.foobar.com:3500', () => { - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() }) cy.visit('/fixtures/primary-origin.html') @@ -501,7 +501,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://www.foobar.com:3500/test-request-credentials', resourceType: 'xhr', credentialStatus: withCredentials, @@ -523,7 +523,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) }) @@ -553,7 +553,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://app.foobar.com:3500/test-request', resourceType: 'xhr', credentialStatus: true, @@ -567,13 +567,13 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout // manually remove the spec bridge iframe to ensure Cypress.state('window') is not already set window.top?.document.getElementById('Spec\ Bridge:\ foobar.com')?.remove() - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="xhr-fetch-requests-onload"]').click() cy.then(() => { - expect(Cypress.backend).to.have.been.calledWith('request:sent:with:credentials', { + expect(Cypress.backendRequestHandler).to.have.been.calledWith('backend:request', 'request:sent:with:credentials', { url: 'http://localhost:3500/foo.bar.baz.json', resourceType: 'xhr', credentialStatus: false, @@ -582,7 +582,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout }) it('does not patch xmlHttpRequest in the spec window or the AUT if the AUT is on the primary', () => { - cy.stub(Cypress, 'backend').log(false).callThrough() + cy.stub(Cypress, 'backendRequestHandler').log(false).callThrough() cy.visit('fixtures/xhr-fetch-requests.html') cy.window().then((win) => { @@ -614,7 +614,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout xhr.send() }) - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) // expect AUT to NOT be patched in primary @@ -636,7 +636,7 @@ describe('src/cross-origin/patches', { browser: '!webkit', defaultCommandTimeout xhr.send() }) - expect(Cypress.backend).not.to.have.been.calledWithMatch('request:sent:with:credentials') + expect(Cypress.backendRequestHandler).not.to.have.been.calledWithMatch('backend:request', 'request:sent:with:credentials') }) }) }) From 0df5fb4912dfb5ea5e0dbc9ba4fc6e803955c920 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 13:29:32 -0500 Subject: [PATCH 17/19] fix tests --- packages/driver/src/cy/commands/prompt/index.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 675957219dcb..38c8589df454 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -64,11 +64,15 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress, cy: Cypress.Cyp export default (Commands, Cypress, cy) => { if (Cypress.config('experimentalPromptCommand')) { - const initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy) + let initializeCloudCyPromptPromise: Promise> | undefined + + if (Cypress.browser.family === 'chromium' && Cypress.browser.name === 'electron') { + initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy) + } Commands.addAll({ async prompt (message: string, options: object = {}) { - if (Cypress.browser.family !== 'chromium' && Cypress.browser.name !== 'electron') { + if (!initializeCloudCyPromptPromise) { // TODO: (cy.prompt) We will look into supporting other browsers (and testing them) // as this is rolled out throw new Error('`cy.prompt()` is not supported in this browser.') From f90abcdf3578b4fe3d60f17e4629d52697812d5c Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 14:24:47 -0500 Subject: [PATCH 18/19] fix tests --- packages/driver/src/cy/commands/prompt/index.ts | 2 +- packages/driver/src/cypress.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 38c8589df454..65c52ec93acb 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -66,7 +66,7 @@ export default (Commands, Cypress, cy) => { if (Cypress.config('experimentalPromptCommand')) { let initializeCloudCyPromptPromise: Promise> | undefined - if (Cypress.browser.family === 'chromium' && Cypress.browser.name === 'electron') { + if (Cypress.browser.family === 'chromium' || Cypress.browser.name === 'electron') { initializeCloudCyPromptPromise = initializeCloudCyPrompt(Cypress, cy) } diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index b0c5380c3fd6..3db227f6c038 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -91,12 +91,13 @@ const isCypressInCypress = document.defaultView !== top const handlePrimaryOriginSocketEvent = (Cypress, backendRequestNamespace: string) => { Cypress.primaryOriginCommunicator.on( backendRequestNamespace, - async ({ args }: { args: [string, any[]] }, { source, responseEvent }) => { + async ({ args: [eventName, ...args] }: { args: [string, any[]] }, { source, responseEvent }) => { let response try { response = await Cypress.backendRequestHandler( backendRequestNamespace, + eventName, ...args, ) } catch (error) { From ad3c467bc98508a1bb607bba663063e2a3ffdce2 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 15:22:32 -0500 Subject: [PATCH 19/19] fix test --- packages/driver/src/cypress.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 3db227f6c038..dd7e47d887d9 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -814,7 +814,7 @@ class $Cypress { return resolve(reply.response) } - return Cypress.emit(backendRequestNamespace, eventName, ...args, fn) + return this.emit(backendRequestNamespace, eventName, ...args, fn) }) }