From 61d17f68206f3ee531d754144dba38f98e91a697 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 00:07:21 -0500 Subject: [PATCH 01/25] chore: add cdp connection to cy prompt --- .../server/lib/browsers/browser-cri-client.ts | 16 ++- packages/server/lib/browsers/chrome.ts | 14 ++- packages/server/lib/browsers/electron.ts | 17 ++- packages/server/lib/browsers/firefox.ts | 4 + packages/server/lib/browsers/index.ts | 8 +- packages/server/lib/browsers/types.ts | 6 +- packages/server/lib/browsers/webkit.ts | 4 + .../lib/cloud/cy-prompt/CyPromptManager.ts | 11 +- packages/server/lib/open_project.ts | 4 + packages/server/lib/project-base.ts | 6 +- packages/server/lib/socket-base.ts | 2 + .../cloud/cy-prompt/test-cy-prompt.ts | 6 +- .../unit/browsers/browser-cri-client_spec.ts | 31 +++-- .../test/unit/browsers/browsers_spec.js | 13 ++ .../server/test/unit/browsers/chrome_spec.js | 76 ++++++++++++ .../test/unit/browsers/electron_spec.js | 113 ++++++++++++------ .../server/test/unit/browsers/firefox_spec.ts | 6 + .../server/test/unit/browsers/webkit_spec.ts | 8 ++ .../cloud/cy-prompt/CyPromptManager_spec.ts | 30 ++++- .../server/test/unit/open_project_spec.js | 9 ++ packages/server/test/unit/project_spec.js | 34 ++++++ packages/server/test/unit/socket_spec.js | 21 ++++ .../src/cy-prompt/cy-prompt-server-types.ts | 21 ++++ 23 files changed, 407 insertions(+), 53 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index fe6aa66851a0..1358b8fb3136 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -8,7 +8,7 @@ import * as errors from '../errors' import type { CypressError } from '@packages/errors' import { CriClient, DEFAULT_NETWORK_ENABLE_OPTIONS } from './cri-client' import { serviceWorkerClientEventHandler, serviceWorkerClientEventHandlerName } from '@packages/proxy/lib/http/util/service-worker-manager' -import type { ProtocolManagerShape } from '@packages/types' +import type { CyPromptManagerShape, ProtocolManagerShape } from '@packages/types' import type { ServiceWorkerEventHandler } from '@packages/proxy/lib/http/util/service-worker-manager' const debug = Debug('cypress:server:browsers:browser-cri-client') @@ -26,6 +26,7 @@ type BrowserCriClientOptions = { browserName: string onAsynchronousError: (err: CypressError) => void protocolManager?: ProtocolManagerShape + cyPromptManager?: CyPromptManagerShape fullyManageTabs?: boolean onServiceWorkerClientEvent: ServiceWorkerEventHandler } @@ -38,6 +39,7 @@ type BrowserCriClientCreateOptions = { onReconnect?: (client: CriClient) => void port: number protocolManager?: ProtocolManagerShape + cyPromptManager?: CyPromptManagerShape onServiceWorkerClientEvent: ServiceWorkerEventHandler } @@ -184,10 +186,12 @@ export class BrowserCriClient { private browserName: string private onAsynchronousError: (err: CypressError) => void private protocolManager?: ProtocolManagerShape + private cyPromptManager?: CyPromptManagerShape private fullyManageTabs?: boolean onServiceWorkerClientEvent: ServiceWorkerEventHandler currentlyAttachedTarget: CriClient | undefined currentlyAttachedProtocolTarget: CriClient | undefined + currentlyAttachedCyPromptTarget: CriClient | undefined // whenever we instantiate the instance we're already connected bc // we receive an underlying CRI connection // TODO: remove "connected" in favor of closing/closed or disconnected @@ -207,6 +211,7 @@ export class BrowserCriClient { this.browserName = options.browserName this.onAsynchronousError = options.onAsynchronousError this.protocolManager = options.protocolManager + this.cyPromptManager = options.cyPromptManager this.fullyManageTabs = options.fullyManageTabs this.onServiceWorkerClientEvent = options.onServiceWorkerClientEvent } @@ -223,6 +228,7 @@ export class BrowserCriClient { * @param options.onReconnect callback for when the browser cri client reconnects to the browser * @param options.port the port to which to connect * @param options.protocolManager the protocol manager to use with the browser cri client + * @param options.cyPromptManager the cy prompt manager to use with the browser cri client * @param options.onServiceWorkerClientEvent callback for when a service worker fetch event is received * @returns a wrapper around the chrome remote interface that is connected to the browser target */ @@ -235,6 +241,7 @@ export class BrowserCriClient { onReconnect, port, protocolManager, + cyPromptManager, onServiceWorkerClientEvent, } = options @@ -259,6 +266,7 @@ export class BrowserCriClient { browserName, onAsynchronousError, protocolManager, + cyPromptManager, fullyManageTabs, onServiceWorkerClientEvent, }) @@ -568,6 +576,12 @@ export class BrowserCriClient { await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) } + // Clone the cy prompt target here so that we separate the cy propt client and the main client. + if (!this.currentlyAttachedCyPromptTarget) { + this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() + await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) + } + return this.currentlyAttachedTarget }, this.browserName, this.port) } diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index c2d969d3b73b..ea00e5401049 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -21,7 +21,7 @@ import type { CriClient } from './cri-client' import type { Automation } from '../automation' import memory from './memory' -import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, RunModeVideoApi } from '@packages/types' +import type { BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types' import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket' import { DEFAULT_CHROME_FLAGS } from '../util/chromium_flags' @@ -412,6 +412,18 @@ export = { await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedProtocolTarget) }, + async connectCyPromptToBrowser (options: { cyPromptManager?: CyPromptManagerShape }) { + const browserCriClient = this._getBrowserCriClient() + + if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectCyPromptToBrowser') + + if (!browserCriClient.currentlyAttachedCyPromptTarget) { + browserCriClient.currentlyAttachedCyPromptTarget = await browserCriClient.currentlyAttachedTarget.clone() + } + + await options.cyPromptManager?.connectToBrowser(browserCriClient.currentlyAttachedCyPromptTarget) + }, + async closeProtocolConnection () { const browserCriClient = this._getBrowserCriClient() diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index 07762ae2b7c7..f7a4860d8052 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -12,7 +12,7 @@ import type { Browser, BrowserInstance, GracefulShutdownOptions } from './types' // tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined import type { BrowserWindow } from 'electron' import type { Automation } from '../automation' -import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, RunModeVideoApi } from '@packages/types' +import type { BrowserLaunchOpts, Preferences, ProtocolManagerShape, CyPromptManagerShape, RunModeVideoApi } from '@packages/types' import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket' import memory from './memory' import { BrowserCriClient } from './browser-cri-client' @@ -276,7 +276,7 @@ export = { return this._launch(win, url, automation, electronOptions) }, - async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManagerShape, cdpSocketServer?: CDPSocketServer) { + async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManagerShape, cyPromptManager?: CyPromptManagerShape, cdpSocketServer?: CDPSocketServer) { if (options.show) { menu.set({ withInternalDevTools: true }) } @@ -500,6 +500,19 @@ export = { await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedProtocolTarget) }, + async connectCyPromptToBrowser (options: { cyPromptManager?: CyPromptManagerShape }) { + const browserCriClient = this._getBrowserCriClient() + + if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectCyPromptToBrowser') + + // Clone the target here so that we separate the cy prompt client and the main client. + if (!browserCriClient.currentlyAttachedCyPromptTarget) { + browserCriClient.currentlyAttachedCyPromptTarget = await browserCriClient.currentlyAttachedTarget.clone() + } + + await options.cyPromptManager?.connectToBrowser(browserCriClient.currentlyAttachedCyPromptTarget) + }, + async closeProtocolConnection () { const browserCriClient = this._getBrowserCriClient() diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index 9cdc2c4e4c9e..34850fc0e3de 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -439,6 +439,10 @@ export function connectProtocolToBrowser (): Promise { throw new Error('Protocol is not yet supported in firefox.') } +export function connectCyPromptToBrowser (): Promise { + throw new Error('CyPrompt is not yet supported in firefox.') +} + export function closeProtocolConnection (): Promise { throw new Error('Protocol is not yet supported in firefox.') } diff --git a/packages/server/lib/browsers/index.ts b/packages/server/lib/browsers/index.ts index b492d76b823a..ed09ac4c0799 100644 --- a/packages/server/lib/browsers/index.ts +++ b/packages/server/lib/browsers/index.ts @@ -7,7 +7,7 @@ import check from 'check-more-types' import { exec } from 'child_process' import util from 'util' import os from 'os' -import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape } from '@packages/types' +import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape, CyPromptManagerShape } from '@packages/types' import type { Browser, BrowserInstance, BrowserLauncher } from './types' import type { Automation } from '../automation' import type { DataContext } from '@packages/data-context' @@ -147,6 +147,12 @@ export = { await browserLauncher.connectProtocolToBrowser(options) }, + async connectCyPromptToBrowser (options: { browser: Browser, foundBrowsers?: FoundBrowser[], cyPromptManager?: CyPromptManagerShape }) { + const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || []) + + await browserLauncher.connectCyPromptToBrowser(options) + }, + async closeProtocolConnection (options: { browser: Browser, foundBrowsers?: FoundBrowser[] }) { const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || []) diff --git a/packages/server/lib/browsers/types.ts b/packages/server/lib/browsers/types.ts index d0579439d622..59668bd41915 100644 --- a/packages/server/lib/browsers/types.ts +++ b/packages/server/lib/browsers/types.ts @@ -1,4 +1,4 @@ -import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape } from '@packages/types' +import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape, CyPromptManagerShape } from '@packages/types' import type { EventEmitter } from 'events' import type { Automation } from '../automation' import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket' @@ -45,6 +45,10 @@ export type BrowserLauncher = { * Used to connect the protocol to an existing browser. */ connectProtocolToBrowser: (options: { protocolManager?: ProtocolManagerShape }) => Promise + /** + * Used to connect the cy prompt to an existing browser. + */ + connectCyPromptToBrowser: (options: { cyPromptManager?: CyPromptManagerShape }) => Promise /** * Closes the protocol connection to the browser. */ diff --git a/packages/server/lib/browsers/webkit.ts b/packages/server/lib/browsers/webkit.ts index 794990cedccc..22ccf58a3302 100644 --- a/packages/server/lib/browsers/webkit.ts +++ b/packages/server/lib/browsers/webkit.ts @@ -40,6 +40,10 @@ export function connectProtocolToBrowser (): Promise { throw new Error('Protocol is not yet supported in WebKit.') } +export function connectCyPromptToBrowser (): Promise { + throw new Error('CyPrompt is not yet supported in WebKit.') +} + export function closeProtocolConnection (): Promise { throw new Error('Protocol is not yet supported in WebKit.') } diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts index ce247d5a156f..c3d08058b4d6 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptManager.ts @@ -1,4 +1,4 @@ -import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, CyPromptServerShape, CyPromptCloudApi } from '@packages/types' +import type { CyPromptManagerShape, CyPromptStatus, CyPromptServerDefaultShape, CyPromptServerShape, CyPromptCloudApi, CyPromptCDPClient } from '@packages/types' import type { Router } from 'express' import Debug from 'debug' import { requireScript } from '../require_script' @@ -40,7 +40,13 @@ export class CyPromptManager implements CyPromptManagerShape { async handleBackendRequest (eventName: string, ...args: any[]): Promise { if (this._cyPromptServer) { - return this.invokeAsync('handleBackendRequest', { isEssential: true }, eventName, ...args) + return this.invokeAsync('handleBackendRequest', { isEssential: false }, eventName, ...args) + } + } + + connectToBrowser (target: CyPromptCDPClient): void { + if (this._cyPromptServer) { + return this.invokeSync('connectToBrowser', { isEssential: true }, target) } } @@ -54,6 +60,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/open_project.ts b/packages/server/lib/open_project.ts index 797eeac8f590..0f60e82c5189 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -235,6 +235,10 @@ export class OpenProject { await browsers.connectProtocolToBrowser(options) } + async connectCyPromptToBrowser (options) { + await browsers.connectCyPromptToBrowser(options) + } + changeUrlToSpec (spec: Cypress.Spec) { if (!this.projectBase) { debug('No projectBase, cannot change url') diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 42d0986d6477..a22fb3a75eee 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -17,7 +17,7 @@ import { SocketCt } from './socket-ct' import { SocketE2E } from './socket-e2e' import { ensureProp } from './util/class-helpers' import system from './util/system' -import { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands, StudioMetricsTypes } from '@packages/types' +import { BannersState, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, ProtocolManagerShape, CyPromptManagerShape, ReceivedCypressOptions, ResolvedConfigurationOptions, TestingType, VideoRecording, AutomationCommands, StudioMetricsTypes } from '@packages/types' import { DataContext, getCtx } from '@packages/data-context' import { createHmac } from 'crypto' import { ServerBase } from './server-base' @@ -515,6 +515,10 @@ export class ProjectBase extends EE { } }, + onCyPromptReady: (cyPromptManager: CyPromptManagerShape) => { + browsers.connectCyPromptToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, cyPromptManager }) + }, + onCaptureVideoFrames: (data: any) => { // TODO: move this to browser automation middleware this.emit('capture:video:frames', data) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index e99e18f5d2bd..9bd63fef3a14 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -154,6 +154,7 @@ export class SocketBase { onCaptureVideoFrames () {}, onStudioInit () {}, onStudioDestroy () {}, + onCyPromptReady () {}, }) let automationClient @@ -451,6 +452,7 @@ export class SocketBase { let cyPrompt: CyPromptManagerShape | undefined getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cp) => { + options.onCyPromptReady(cp) cyPrompt = cp }) 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..cde602ab31ff 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 @@ -1,6 +1,6 @@ /// -import type { CyPromptServerShape, CyPromptServerDefaultShape } from '@packages/types' +import type { CyPromptServerShape, CyPromptServerDefaultShape, CyPromptCDPClient } from '@packages/types' import type { Router } from 'express' class CyPromptServer implements CyPromptServerShape { @@ -11,6 +11,10 @@ class CyPromptServer implements CyPromptServerShape { handleBackendRequest (eventName: string, ...args: any[]): Promise { return Promise.resolve() } + + connectToBrowser (criClient: CyPromptCDPClient): void { + // This is a test implementation that does nothing + } } const cyPromptServerDefault: CyPromptServerDefaultShape = { diff --git a/packages/server/test/unit/browsers/browser-cri-client_spec.ts b/packages/server/test/unit/browsers/browser-cri-client_spec.ts index 39e18c66ad1a..7258149346fc 100644 --- a/packages/server/test/unit/browsers/browser-cri-client_spec.ts +++ b/packages/server/test/unit/browsers/browser-cri-client_spec.ts @@ -4,7 +4,7 @@ import { expect, proxyquire, sinon } from '../../spec_helper' import * as protocol from '../../../lib/browsers/protocol' import { stripAnsi } from '@packages/errors' import net from 'net' -import { ProtocolManagerShape } from '@packages/types' +import { ProtocolManagerShape, CyPromptManagerShape } from '@packages/types' import type { Protocol } from 'devtools-protocol' import { serviceWorkerClientEventHandlerName } from '@packages/proxy/lib/http/util/service-worker-manager' @@ -14,6 +14,7 @@ const THROWS_PORT = 65535 type GetClientParams = { protocolManager?: ProtocolManagerShape + cyPromptManager?: CyPromptManagerShape fullyManageTabs?: boolean } @@ -59,14 +60,14 @@ describe('lib/browsers/browser-cri-client', function () { 'chrome-remote-interface': criImport, }) - getClient = ({ protocolManager, fullyManageTabs } = {}) => { + getClient = ({ protocolManager, cyPromptManager, fullyManageTabs } = {}) => { criClientCreateStub = criClientCreateStub.withArgs({ target: 'http://web/socket/url', onAsynchronousError: onError, onReconnect: undefined, protocolManager, fullyManageTabs }).resolves({ send, on, close, }) - return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, fullyManageTabs, onServiceWorkerClientEvent }) + return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, cyPromptManager, fullyManageTabs, onServiceWorkerClientEvent }) } }) @@ -450,8 +451,9 @@ describe('lib/browsers/browser-cri-client', function () { context('#attachToTargetUrl', function () { it('creates a page client when the passed in url is found', async function () { const mockProtocolClient = {} + const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().returns(mockProtocolClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) @@ -463,39 +465,50 @@ describe('lib/browsers/browser-cri-client', function () { expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) + expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) }) it('creates a page client when the passed in url is found and notifies the protocol manager and fully managed tabs', async function () { const mockProtocolClient = {} + const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().returns(mockProtocolClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), } const protocolManager: any = { connectToBrowser: sinon.stub().resolves(), } + const cyPromptManager: any = { + connectToBrowser: sinon.stub().resolves(), + } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) send.withArgs('Target.setDiscoverTargets', { discover: true }) on.withArgs('Target.targetDestroyed', sinon.match.func) criClientCreateStub.withArgs({ target: '1', onAsynchronousError: onError, host: HOST, port: PORT, protocolManager, fullyManageTabs: true, browserClient: { on, send, close } }).resolves(mockPageClient) - const browserClient = await getClient({ protocolManager, fullyManageTabs: true }) + const browserClient = await getClient({ protocolManager, cyPromptManager, fullyManageTabs: true }) const client = await browserClient.attachToTargetUrl('http://foo.com') expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) + expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) expect(protocolManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedProtocolTarget) + expect(cyPromptManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedCyPromptTarget) }) it('creates a page client when the passed in url is found and notifies the protocol manager and fully managed tabs and attaching to target throws', async function () { const mockProtocolClient = {} + const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().returns(mockProtocolClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), } const protocolManager: any = { connectToBrowser: sinon.stub().resolves(), } + const cyPromptManager: any = { + connectToBrowser: sinon.stub().resolves(), + } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) send.withArgs('Target.setDiscoverTargets', { discover: true }) @@ -505,13 +518,15 @@ describe('lib/browsers/browser-cri-client', function () { criClientCreateStub.withArgs({ target: '1', onAsynchronousError: onError, host: HOST, port: PORT, protocolManager, fullyManageTabs: true, browserClient: { on, send, close } }).resolves(mockPageClient) - const browserClient = await getClient({ protocolManager, fullyManageTabs: true }) + const browserClient = await getClient({ protocolManager, cyPromptManager, fullyManageTabs: true }) const client = await browserClient.attachToTargetUrl('http://foo.com') expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) + expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) expect(protocolManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedProtocolTarget) + expect(cyPromptManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedCyPromptTarget) // This would throw if the error was not caught await on.withArgs('Target.attachedToTarget').args[0][1]({ targetInfo: { type: 'worker' } }) diff --git a/packages/server/test/unit/browsers/browsers_spec.js b/packages/server/test/unit/browsers/browsers_spec.js index a7ec2835070b..811e7d266434 100644 --- a/packages/server/test/unit/browsers/browsers_spec.js +++ b/packages/server/test/unit/browsers/browsers_spec.js @@ -118,6 +118,19 @@ describe('lib/browsers/index', () => { }) }) + context('.connectCyPromptToBrowser', () => { + it('connects browser to cy prompt', async () => { + sinon.stub(chrome, 'connectCyPromptToBrowser').resolves() + await browsers.connectCyPromptToBrowser({ + browser: { + family: 'chromium', + }, + }) + + expect(chrome.connectCyPromptToBrowser).to.be.called + }) + }) + context('.closeProtocolConnection', () => { it('calls close on instance', async () => { sinon.stub(chrome, 'closeProtocolConnection').resolves() diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 62436c7a4670..fa4bfb73a569 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -665,6 +665,82 @@ describe('lib/browsers/chrome', () => { }) }) + context('#connectCyPromptToBrowser', () => { + it('connects to the browser cri client', async function () { + const cyPromptManager = { + connectToBrowser: sinon.stub().resolves(), + } + + const mockCurrentlyAttachedCyPromptTarget = {} + + const pageCriClient = { + clone: sinon.stub().returns(mockCurrentlyAttachedCyPromptTarget), + } + + const browserCriClient = { + currentlyAttachedTarget: pageCriClient, + currentlyAttachedCyPromptTarget: mockCurrentlyAttachedCyPromptTarget, + } + + sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient) + + await chrome.connectCyPromptToBrowser({ cyPromptManager }) + + expect(pageCriClient.clone).not.to.be.called + expect(cyPromptManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedCyPromptTarget) + }) + + it('connects to the browser cri client when the cy prompt target has not been created', async function () { + const cyPromptManager = { + connectToBrowser: sinon.stub().resolves(), + } + + const mockCurrentlyAttachedCyPromptTarget = {} + + const pageCriClient = { + clone: sinon.stub().resolves(mockCurrentlyAttachedCyPromptTarget), + } + + const browserCriClient = { + currentlyAttachedTarget: pageCriClient, + } + + sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient) + + await chrome.connectCyPromptToBrowser({ cyPromptManager }) + + expect(pageCriClient.clone).to.be.called + expect(cyPromptManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedCyPromptTarget) + expect(browserCriClient.currentlyAttachedCyPromptTarget).to.eq(mockCurrentlyAttachedCyPromptTarget) + }) + + it('throws error if there is no browser cri client', function () { + const cyPromptManager = { + connectToBrowser: sinon.stub().resolves(), + } + + sinon.stub(chrome, '_getBrowserCriClient').returns(null) + + expect(chrome.connectCyPromptToBrowser({ cyPromptManager })).to.be.rejectedWith('Missing pageCriClient in connectCyPromptToBrowser') + expect(cyPromptManager.connectToBrowser).not.to.be.called + }) + + it('throws error if there is no page cri client', function () { + const cyPromptManager = { + connectToBrowser: sinon.stub().resolves(), + } + + const browserCriClient = { + currentlyAttachedTarget: null, + } + + sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient) + + expect(chrome.connectCyPromptToBrowser({ cyPromptManager })).to.be.rejectedWith('Missing pageCriClient in connectCyPromptToBrowser') + expect(cyPromptManager.connectToBrowser).not.to.be.called + }) + }) + context('#closeProtocolConnection', () => { it('closes the protocol connection', async function () { const mockCurrentlyAttachedProtocolTarget = { diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index 338db60b46d8..1c0e089d3d29 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -24,6 +24,10 @@ describe('lib/browsers/electron', () => { connectToBrowser: sinon.stub().resolves(), } + this.cyPromptManager = { + connectToBrowser: sinon.stub().resolves(), + } + this.url = 'https://foo.com' this.state = {} this.options = { @@ -280,6 +284,47 @@ describe('lib/browsers/electron', () => { }) }) + context('.connectCyPromptToBrowser', () => { + it('connects to the browser cri client', async function () { + const mockCurrentlyAttachedCyPromptTarget = {} + + this.browserCriClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget + sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient) + + await electron.connectCyPromptToBrowser({ cyPromptManager: this.cyPromptManager }) + expect(this.pageCriClient.clone).not.to.be.called + expect(this.cyPromptManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedCyPromptTarget) + }) + + it('connects to the browser cri client when the cy prompt target has not been created', async function () { + const mockCurrentlyAttachedCyPromptTarget = {} + + this.pageCriClient.clone.resolves(mockCurrentlyAttachedCyPromptTarget) + sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient) + + await electron.connectCyPromptToBrowser({ cyPromptManager: this.cyPromptManager }) + expect(this.pageCriClient.clone).to.be.called + expect(this.cyPromptManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedCyPromptTarget) + expect(this.browserCriClient.currentlyAttachedCyPromptTarget).to.eq(mockCurrentlyAttachedCyPromptTarget) + }) + + it('throws error if there is no browser cri client', function () { + sinon.stub(electron, '_getBrowserCriClient').returns(null) + + expect(electron.connectCyPromptToBrowser({ cyPromptManager: this.cyPromptManager })).to.be.rejectedWith('Missing pageCriClient in connectCyPromptToBrowser') + expect(this.cyPromptManager.connectToBrowser).not.to.be.called + }) + + it('throws error if there is no page cri client', async function () { + this.browserCriClient.currentlyAttachedTarget = null + + sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient) + + expect(electron.connectCyPromptToBrowser({ cyPromptManager: this.cyPromptManager })).to.be.rejectedWith('Missing pageCriClient in connectCyPromptToBrowser') + expect(this.cyPromptManager.connectToBrowser).not.to.be.called + }) + }) + context('#closeProtocolConnection', () => { it('closes the protocol connection', async function () { const mockCurrentlyAttachedProtocolTarget = { @@ -336,7 +381,7 @@ describe('lib/browsers/electron', () => { }) it('sets menu.set whether or not its in headless mode', function () { - return electron._launch(this.win, this.url, this.automation, { show: true, onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { show: true, onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(menu.set).to.be.calledWith({ withInternalDevTools: true }) }).then(() => { @@ -349,36 +394,36 @@ describe('lib/browsers/electron', () => { }) it('sets user agent if options.userAgent', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(electron._setUserAgent).not.to.be.called }).then(() => { - return electron._launch(this.win, this.url, this.automation, { userAgent: 'foo', onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { userAgent: 'foo', onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) }).then(() => { expect(electron._setUserAgent).to.be.calledWith(this.win.webContents, 'foo') }) }) it('sets proxy if options.proxyServer', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(electron._setProxy).not.to.be.called }).then(() => { - return electron._launch(this.win, this.url, this.automation, { proxyServer: 'foo', onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { proxyServer: 'foo', onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) }).then(() => { expect(electron._setProxy).to.be.calledWith(this.win.webContents, 'foo') }) }) it('calls win.loadURL with url', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.loadURL).to.be.calledWith(this.url) }) }) it('resolves with win', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then((win) => { expect(win).to.eq(this.win) }) @@ -397,7 +442,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('create:download', { id: '1', @@ -421,7 +466,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('complete:download', { id: '1', @@ -442,7 +487,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('canceled:download', { id: '1', @@ -453,7 +498,7 @@ describe('lib/browsers/electron', () => { it('sets download behavior', function () { this.options.downloadsFolder = 'downloads' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.pageCriClient.send).to.be.calledWith('Page.setDownloadBehavior', { behavior: 'allow', @@ -463,14 +508,14 @@ describe('lib/browsers/electron', () => { }) it('handles download links via cdp', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(utils.initializeCDP).to.be.calledWith(this.pageCriClient, this.automation) }) }) it('expects the browser to be reset', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.pageCriClient.send).to.be.calledWith('Storage.clearDataForOrigin', { origin: '*', storageTypes: 'all' }) expect(this.pageCriClient.send).to.be.calledWith('Network.clearBrowserCache') @@ -485,7 +530,7 @@ describe('lib/browsers/electron', () => { }), } - await electron._launch(this.win, this.url, this.automation, this.options, mockVideoApi, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, mockVideoApi, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(mockVideoApi.useFfmpegVideoController).to.be.called expect(this.pageCriClient.on).to.be.calledWith('Page.screencastFrame', sinon.match.func) @@ -495,7 +540,7 @@ describe('lib/browsers/electron', () => { it('starts the screencast but does not capture the frames if video is not enabled but the app is in run mode', async function () { this.options.isTextTerminal = true - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func) expect(this.pageCriClient.send).to.be.calledWith('Page.startScreencast', { @@ -506,7 +551,7 @@ describe('lib/browsers/electron', () => { }) it('does not start the screencast if video is not enabled and the app is not in run mode', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func) expect(this.pageCriClient.send).not.to.be.calledWith('Page.startScreencast', sinon.match.any) @@ -515,7 +560,7 @@ describe('lib/browsers/electron', () => { it('registers onRequest automation middleware and calls show when requesting to be focused', function () { sinon.spy(this.automation, 'use') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.use).to.be.called expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function') @@ -529,7 +574,7 @@ describe('lib/browsers/electron', () => { it('registers onRequest automation middleware and calls destroy when requesting to close the browser tabs', function () { sinon.spy(this.automation, 'use') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(async () => { expect(this.automation.use).to.be.called expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function') @@ -565,7 +610,7 @@ describe('lib/browsers/electron', () => { }) it('sends Fetch.enable only for Document ResourceType', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', { patterns: [{ @@ -575,7 +620,7 @@ describe('lib/browsers/electron', () => { }) it('does not add header when not a document', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Fetch.requestPaused').yield({ requestId: '1234', @@ -586,7 +631,7 @@ describe('lib/browsers/electron', () => { }) it('does not add header when it is a spec frame request', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -605,7 +650,7 @@ describe('lib/browsers/electron', () => { }) it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -637,7 +682,7 @@ describe('lib/browsers/electron', () => { }) it('gets frame tree on Page.frameAttached', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -645,7 +690,7 @@ describe('lib/browsers/electron', () => { }) it('gets frame tree on Page.frameDetached', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameDetached').yield() @@ -657,7 +702,7 @@ describe('lib/browsers/electron', () => { this.pageCriClient.clone.resolves(mockCurrentlyAttachedProtocolTarget) - await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager, undefined, { attachCDPClient: sinon.stub() }) expect(this.protocolManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedProtocolTarget) }) @@ -676,7 +721,7 @@ describe('lib/browsers/electron', () => { it('does not attempt to replace the user agent', function () { this.options.experimentalModifyObstructiveThirdPartyCode = false - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.webContents.session.setUserAgent).not.to.be.called expect(this.pageCriClient.send).not.to.be.calledWith('Network.setUserAgentOverride', { @@ -696,7 +741,7 @@ describe('lib/browsers/electron', () => { this.options.experimentalModifyObstructiveThirdPartyCode = false this.options.userAgent = 'foobar' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.webContents.session.setUserAgent).to.be.calledWith('foobar') expect(this.win.webContents.session.setUserAgent).not.to.be.calledWith('barbaz') @@ -709,7 +754,7 @@ describe('lib/browsers/electron', () => { it('versioned cypress', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/10.0.3 Chrome/100.0.4896.75 Electron/18.0.4 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' @@ -723,7 +768,7 @@ describe('lib/browsers/electron', () => { it('development cypress', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/0.0.0-development Chrome/100.0.4896.75 Electron/18.0.4 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' @@ -737,7 +782,7 @@ describe('lib/browsers/electron', () => { it('older Windows user agent', function () { userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.113 Safari/537.36' @@ -751,7 +796,7 @@ describe('lib/browsers/electron', () => { it('newer Windows user agent', function () { userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.5.00.4689 Chrome/85.0.4183.121 Electron/10.4.7 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.5.00.4689 Chrome/85.0.4183.121 Safari/537.36' @@ -765,7 +810,7 @@ describe('lib/browsers/electron', () => { it('Linux user agent', function () { userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Typora/0.9.93 Chrome/83.0.4103.119 Electron/9.0.5 Safari/E7FBAF' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Typora/0.9.93 Chrome/83.0.4103.119 Safari/E7FBAF' @@ -780,7 +825,7 @@ describe('lib/browsers/electron', () => { // this user agent containing Cypress was actually a common UA found on a website for Electron purposes... userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/8.3.0 Chrome/91.0.4472.124 Electron/13.1.7 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' @@ -794,7 +839,7 @@ describe('lib/browsers/electron', () => { it('newer MacOS user agent', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index a7d1b76c65cc..f89a46112129 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -684,6 +684,12 @@ describe('lib/browsers/firefox', () => { }) }) + context('#connectCyPromptToBrowser', () => { + it('throws error', () => { + expect(firefox.connectCyPromptToBrowser).to.throw('CyPrompt is not yet supported in firefox.') + }) + }) + context('#closeProtocolConnection', () => { it('throws error', () => { expect(firefox.closeProtocolConnection).to.throw('Protocol is not yet supported in firefox.') diff --git a/packages/server/test/unit/browsers/webkit_spec.ts b/packages/server/test/unit/browsers/webkit_spec.ts index bff4d0f7d490..97a9f9e7994c 100644 --- a/packages/server/test/unit/browsers/webkit_spec.ts +++ b/packages/server/test/unit/browsers/webkit_spec.ts @@ -78,6 +78,14 @@ describe('lib/browsers/webkit', () => { }) }) + context('#connectCyPromptToBrowser', () => { + it('throws error', () => { + const webkit = getWebkit() + + expect(webkit.connectCyPromptToBrowser).to.throw('CyPrompt is not yet supported in WebKit.') + }) + }) + context('#closeProtocolConnection', () => { it('throws error', async () => { const webkit = getWebkit() 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..905f8c73d4aa 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts @@ -52,6 +52,8 @@ describe('lib/cloud/cy-prompt', () => { cyPromptManager.initializeRoutes({} as any) expect(cyPromptManager.status).to.eq('IN_ERROR') + + // TODO: test that the error is reported }) }) @@ -63,7 +65,7 @@ describe('lib/cloud/cy-prompt', () => { await cyPromptManager.handleBackendRequest('cy:prompt:start', {} as any) - expect(cyPromptManager.status).to.eq('IN_ERROR') + // TODO: test that the error is reported }) }) @@ -99,4 +101,30 @@ describe('lib/cloud/cy-prompt', () => { expect(invokeSyncSpy).to.not.be.called }) }) + + describe('connectToBrowser', () => { + it('connects to the browser', () => { + const mockCriClient = { + send: sinon.stub().resolves(), + on: sinon.stub().resolves(), + } + + sinon.stub(cyPrompt, 'connectToBrowser') + + cyPromptManager.connectToBrowser(mockCriClient) + + expect(cyPrompt.connectToBrowser).to.be.calledWith(mockCriClient) + }) + + it('does not call connectToBrowser when cy prompt server is not defined', () => { + // Set _cyPromptServer to undefined + (cyPromptManager as any)._cyPromptServer = undefined + + const invokeSyncSpy = sinon.spy(cyPromptManager, 'invokeSync') + + cyPromptManager.connectToBrowser({} as any) + + expect(invokeSyncSpy).to.not.be.called + }) + }) }) diff --git a/packages/server/test/unit/open_project_spec.js b/packages/server/test/unit/open_project_spec.js index ad0ed0f067fb..6f7e489d569c 100644 --- a/packages/server/test/unit/open_project_spec.js +++ b/packages/server/test/unit/open_project_spec.js @@ -266,4 +266,13 @@ describe('lib/open_project', () => { expect(browsers.connectProtocolToBrowser).to.be.calledWith(options) }) }) + + context('#connectCyPromptToBrowser', () => { + it('connects cy prompt to browser', async () => { + sinon.stub(browsers, 'connectCyPromptToBrowser').resolves() + const options = sinon.stub() + + await openProject.connectCyPromptToBrowser(options) + }) + }) }) diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index 448a121618aa..110956978a0e 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -1038,6 +1038,40 @@ This option will not have an effect in Some-other-name. Tests that rely on web s expect(protocolManager.close).to.have.been.calledOnce expect(this.project['_protocolManager']).to.be.undefined }) + + it('passes onCyPromptReady callback', async function () { + const mockCyPromptManager = { + foo: 'bar', + } + + // Create a browser object + this.project.browser = { + name: 'chrome', + family: 'chromium', + } + + this.project.options = { browsers: [this.project.browser] } + + sinon.stub(browsers, 'connectCyPromptToBrowser') + + // Modify the startWebsockets stub to track the callbacks + const callbackPromise = new Promise((resolve) => { + this.project.server.startWebsockets.callsFake(async (automation, config, callbacks) => { + await callbacks.onCyPromptReady(mockCyPromptManager) + resolve() + }) + }) + + this.project.startWebsockets({}, {}) + + await callbackPromise + + expect(browsers.connectCyPromptToBrowser).to.have.been.calledWith({ + browser: this.project.browser, + foundBrowsers: this.project.options.browsers, + cyPromptManager: mockCyPromptManager, + }) + }) }) context('#getProjectId', () => { diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index e22b260b9774..b8204bb6ed04 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -66,6 +66,7 @@ describe('lib/socket', () => { onSavedStateChanged: sinon.spy(), onStudioInit: sinon.stub(), onStudioDestroy: sinon.stub(), + onCyPromptReady: sinon.stub(), } this.automation = new Automation({ @@ -684,6 +685,26 @@ describe('lib/socket', () => { }) }) + context('cy.prompt.addSocketListeners', () => { + it('calls onCyPromptReady with the cy prompt manager', function () { + const mockCyPromptManager = { + foo: 'bar', + } + + // Verify that registerCyPromptReadyListener was called + expect(ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener).to.be.called + + // Check that the callback was called with the mock cy prompt manager object + const registerCyPromptReadyListenerCallback = ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener.firstCall.args[0] + + expect(registerCyPromptReadyListenerCallback).to.be.a('function') + + registerCyPromptReadyListenerCallback(mockCyPromptManager) + + expect(this.options.onCyPromptReady).to.be.calledWith(mockCyPromptManager) + }) + }) + 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..be0df63c42ad 100644 --- a/packages/types/src/cy-prompt/cy-prompt-server-types.ts +++ b/packages/types/src/cy-prompt/cy-prompt-server-types.ts @@ -4,9 +4,18 @@ /// +import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping.d' import type { Router } from 'express' import type { AxiosInstance } from 'axios' +export type CyPromptCommands = ProtocolMapping.Commands + +export type CyPromptCommand = CyPromptCommands[T] + +export type CyPromptEvents = ProtocolMapping.Events + +export type CyPromptEvent = CyPromptEvents[T] + interface RetryOptions { maxAttempts: number retryDelay?: (attempt: number) => number @@ -34,9 +43,21 @@ export interface CyPromptServerOptions { cloudApi: CyPromptCloudApi } +export interface CyPromptCDPClient { + send>( + command: T, + params?: CyPromptCommand['paramsType'][0] + ): Promise['returnType']> + on>( + eventName: T, + cb: (event: CyPromptEvent[0]) => void | Promise + ): void +} + export interface CyPromptServerShape { initializeRoutes(router: Router): void handleBackendRequest: (eventName: string, ...args: any[]) => Promise + connectToBrowser: (cdpClient: CyPromptCDPClient) => void } export interface CyPromptServerDefaultShape { From 0f1dcf9e8aebd78c647d41aedcae5f29e616ab34 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 00:31:44 -0500 Subject: [PATCH 02/25] minor fix --- packages/server/lib/browsers/electron.ts | 2 +- .../test/unit/browsers/electron_spec.js | 68 +++++++++---------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index f7a4860d8052..fb75f9d7593e 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -276,7 +276,7 @@ export = { return this._launch(win, url, automation, electronOptions) }, - async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManagerShape, cyPromptManager?: CyPromptManagerShape, cdpSocketServer?: CDPSocketServer) { + async _launch (win: BrowserWindow, url: string, automation: Automation, options: ElectronOpts, videoApi?: RunModeVideoApi, protocolManager?: ProtocolManagerShape, cdpSocketServer?: CDPSocketServer) { if (options.show) { menu.set({ withInternalDevTools: true }) } diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index 1c0e089d3d29..5c679f250d82 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -381,7 +381,7 @@ describe('lib/browsers/electron', () => { }) it('sets menu.set whether or not its in headless mode', function () { - return electron._launch(this.win, this.url, this.automation, { show: true, onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { show: true, onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(menu.set).to.be.calledWith({ withInternalDevTools: true }) }).then(() => { @@ -394,36 +394,36 @@ describe('lib/browsers/electron', () => { }) it('sets user agent if options.userAgent', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(electron._setUserAgent).not.to.be.called }).then(() => { - return electron._launch(this.win, this.url, this.automation, { userAgent: 'foo', onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { userAgent: 'foo', onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) }).then(() => { expect(electron._setUserAgent).to.be.calledWith(this.win.webContents, 'foo') }) }) it('sets proxy if options.proxyServer', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(electron._setProxy).not.to.be.called }).then(() => { - return electron._launch(this.win, this.url, this.automation, { proxyServer: 'foo', onError: () => {} }, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, { proxyServer: 'foo', onError: () => {} }, undefined, undefined, { attachCDPClient: sinon.stub() }) }).then(() => { expect(electron._setProxy).to.be.calledWith(this.win.webContents, 'foo') }) }) it('calls win.loadURL with url', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.loadURL).to.be.calledWith(this.url) }) }) it('resolves with win', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then((win) => { expect(win).to.eq(this.win) }) @@ -442,7 +442,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('create:download', { id: '1', @@ -466,7 +466,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('complete:download', { id: '1', @@ -487,7 +487,7 @@ describe('lib/browsers/electron', () => { this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.push).to.be.calledWith('canceled:download', { id: '1', @@ -498,7 +498,7 @@ describe('lib/browsers/electron', () => { it('sets download behavior', function () { this.options.downloadsFolder = 'downloads' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.pageCriClient.send).to.be.calledWith('Page.setDownloadBehavior', { behavior: 'allow', @@ -508,14 +508,14 @@ describe('lib/browsers/electron', () => { }) it('handles download links via cdp', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(utils.initializeCDP).to.be.calledWith(this.pageCriClient, this.automation) }) }) it('expects the browser to be reset', function () { - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.pageCriClient.send).to.be.calledWith('Storage.clearDataForOrigin', { origin: '*', storageTypes: 'all' }) expect(this.pageCriClient.send).to.be.calledWith('Network.clearBrowserCache') @@ -530,7 +530,7 @@ describe('lib/browsers/electron', () => { }), } - await electron._launch(this.win, this.url, this.automation, this.options, mockVideoApi, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, mockVideoApi, undefined, { attachCDPClient: sinon.stub() }) expect(mockVideoApi.useFfmpegVideoController).to.be.called expect(this.pageCriClient.on).to.be.calledWith('Page.screencastFrame', sinon.match.func) @@ -540,7 +540,7 @@ describe('lib/browsers/electron', () => { it('starts the screencast but does not capture the frames if video is not enabled but the app is in run mode', async function () { this.options.isTextTerminal = true - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func) expect(this.pageCriClient.send).to.be.calledWith('Page.startScreencast', { @@ -551,7 +551,7 @@ describe('lib/browsers/electron', () => { }) it('does not start the screencast if video is not enabled and the app is not in run mode', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.on).not.to.be.calledWith('Page.screencastFrame', sinon.match.func) expect(this.pageCriClient.send).not.to.be.calledWith('Page.startScreencast', sinon.match.any) @@ -560,7 +560,7 @@ describe('lib/browsers/electron', () => { it('registers onRequest automation middleware and calls show when requesting to be focused', function () { sinon.spy(this.automation, 'use') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.automation.use).to.be.called expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function') @@ -574,7 +574,7 @@ describe('lib/browsers/electron', () => { it('registers onRequest automation middleware and calls destroy when requesting to close the browser tabs', function () { sinon.spy(this.automation, 'use') - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(async () => { expect(this.automation.use).to.be.called expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function') @@ -610,7 +610,7 @@ describe('lib/browsers/electron', () => { }) it('sends Fetch.enable only for Document ResourceType', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) expect(this.pageCriClient.send).to.have.been.calledWith('Fetch.enable', { patterns: [{ @@ -620,7 +620,7 @@ describe('lib/browsers/electron', () => { }) it('does not add header when not a document', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Fetch.requestPaused').yield({ requestId: '1234', @@ -631,7 +631,7 @@ describe('lib/browsers/electron', () => { }) it('does not add header when it is a spec frame request', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -650,7 +650,7 @@ describe('lib/browsers/electron', () => { }) it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -682,7 +682,7 @@ describe('lib/browsers/electron', () => { }) it('gets frame tree on Page.frameAttached', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameAttached').yield() @@ -690,7 +690,7 @@ describe('lib/browsers/electron', () => { }) it('gets frame tree on Page.frameDetached', async function () { - await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) this.pageCriClient.on.withArgs('Page.frameDetached').yield() @@ -702,7 +702,7 @@ describe('lib/browsers/electron', () => { this.pageCriClient.clone.resolves(mockCurrentlyAttachedProtocolTarget) - await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager, undefined, { attachCDPClient: sinon.stub() }) + await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager, { attachCDPClient: sinon.stub() }) expect(this.protocolManager.connectToBrowser).to.be.calledWith(mockCurrentlyAttachedProtocolTarget) }) @@ -721,7 +721,7 @@ describe('lib/browsers/electron', () => { it('does not attempt to replace the user agent', function () { this.options.experimentalModifyObstructiveThirdPartyCode = false - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.webContents.session.setUserAgent).not.to.be.called expect(this.pageCriClient.send).not.to.be.calledWith('Network.setUserAgentOverride', { @@ -741,7 +741,7 @@ describe('lib/browsers/electron', () => { this.options.experimentalModifyObstructiveThirdPartyCode = false this.options.userAgent = 'foobar' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { expect(this.win.webContents.session.setUserAgent).to.be.calledWith('foobar') expect(this.win.webContents.session.setUserAgent).not.to.be.calledWith('barbaz') @@ -754,7 +754,7 @@ describe('lib/browsers/electron', () => { it('versioned cypress', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/10.0.3 Chrome/100.0.4896.75 Electron/18.0.4 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' @@ -768,7 +768,7 @@ describe('lib/browsers/electron', () => { it('development cypress', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/0.0.0-development Chrome/100.0.4896.75 Electron/18.0.4 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' @@ -782,7 +782,7 @@ describe('lib/browsers/electron', () => { it('older Windows user agent', function () { userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) electron/1.0.0 Chrome/53.0.2785.113 Electron/1.4.3 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.113 Safari/537.36' @@ -796,7 +796,7 @@ describe('lib/browsers/electron', () => { it('newer Windows user agent', function () { userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.5.00.4689 Chrome/85.0.4183.121 Electron/10.4.7 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Teams/1.5.00.4689 Chrome/85.0.4183.121 Safari/537.36' @@ -810,7 +810,7 @@ describe('lib/browsers/electron', () => { it('Linux user agent', function () { userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Typora/0.9.93 Chrome/83.0.4103.119 Electron/9.0.5 Safari/E7FBAF' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Typora/0.9.93 Chrome/83.0.4103.119 Safari/E7FBAF' @@ -825,7 +825,7 @@ describe('lib/browsers/electron', () => { // this user agent containing Cypress was actually a common UA found on a website for Electron purposes... userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/8.3.0 Chrome/91.0.4472.124 Electron/13.1.7 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' @@ -839,7 +839,7 @@ describe('lib/browsers/electron', () => { it('newer MacOS user agent', function () { userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' - return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, undefined, { attachCDPClient: sinon.stub() }) + return electron._launch(this.win, this.url, this.automation, this.options, undefined, undefined, { attachCDPClient: sinon.stub() }) .then(() => { const expectedUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36' From e29803454429f88dcb179918a2f7992554794f57 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 17:52:43 -0500 Subject: [PATCH 03/25] fix type build --- .../cy-prompt/CyPromptLifecycleManager.ts | 12 ++++++---- packages/server/lib/routes.ts | 2 +- packages/server/lib/socket-base.ts | 4 ++-- .../CyPromptLifecycleManager_spec.ts | 2 +- packages/server/test/unit/socket_spec.js | 22 +++++++++---------- packages/types/src/cy-prompt/index.ts | 2 +- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index e7bce3cd41cb..b4e308e7b320 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -16,7 +16,8 @@ const debug = Debug('cypress:server:cy-prompt-lifecycle-manager') export class CyPromptLifecycleManager { private cyPromptManagerPromise?: Promise private cyPromptManager?: CyPromptManager - private listeners: ((cyPromptManager: CyPromptManager) => void)[] = [] + private listeners: ((cyPromptManager: CyPromptManager) => Promise)[] = [] + private listenerPromises: Promise[] = [] /** * Initialize the cy prompt manager. @@ -133,6 +134,9 @@ export class CyPromptLifecycleManager { this.cyPromptManager = cyPromptManager this.callRegisteredListeners() + await Promise.all(this.listenerPromises) + this.listenerPromises = [] + return cyPromptManager } @@ -145,7 +149,7 @@ export class CyPromptLifecycleManager { debug('Calling all cy prompt ready listeners') this.listeners.forEach((listener) => { - listener(cyPromptManager) + this.listenerPromises.push(listener(cyPromptManager)) }) this.listeners = [] @@ -155,11 +159,11 @@ export class CyPromptLifecycleManager { * Register a listener that will be called when the cy prompt manager is ready * @param listener Function to call when cy prompt manager is ready */ - registerCyPromptReadyListener (listener: (cyPromptManager: CyPromptManager) => void): void { + registerCyPromptReadyListener (listener: (cyPromptManager: CyPromptManager) => Promise): void { // if there is already a cy prompt manager, call the listener immediately if (this.cyPromptManager) { debug('cy prompt ready - calling listener immediately') - listener(this.cyPromptManager) + this.listenerPromises.push(listener(this.cyPromptManager)) } else { debug('cy prompt not ready - registering cy prompt ready listener') this.listeners.push(listener) diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 1c7f9fdfd487..fff37f2282fc 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -127,7 +127,7 @@ export const createCommonRoutes = ({ const cyPromptRouter = Router() router.use('/', cyPromptRouter) - getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cyPrompt) => { + getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cyPrompt) => { cyPrompt.initializeRoutes(cyPromptRouter) }) } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 9bd63fef3a14..25c06fc525fd 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -451,8 +451,8 @@ export class SocketBase { let cyPrompt: CyPromptManagerShape | undefined - getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cp) => { - options.onCyPromptReady(cp) + getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cp) => { + await options.onCyPromptReady(cp) cyPrompt = cp }) diff --git a/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts b/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts index bbe38fb9beed..7f6d4df6e7e9 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptLifecycleManager_spec.ts @@ -91,7 +91,7 @@ describe('CyPromptLifecycleManager', () => { }) const cyPromptReadyPromise = new Promise((resolve) => { - cyPromptLifecycleManager?.registerCyPromptReadyListener((cyPromptManager) => { + cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cyPromptManager) => { resolve(cyPromptManager) }) }) diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index b8204bb6ed04..1cbd507e0113 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -579,25 +579,25 @@ describe('lib/socket', () => { }) context('on(backend:request, cy:prompt)', () => { - it('calls handleBackendRequest with the correct arguments', function (done) { + it('calls handleBackendRequest with the correct arguments', async function () { // 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) + await registerCyPromptReadyListenerCallback(mockCyPrompt) - return this.client.emit('backend:request', 'cy:prompt:init', 'foo', (resp) => { - expect(resp.response).to.deep.eq({ foo: 'bar' }) - expect(mockCyPrompt.handleBackendRequest).to.be.calledWith('cy:prompt:init', 'foo') + await new Promise((resolve) => { + this.client.emit('backend:request', 'cy:prompt:init', 'foo', (resp) => { + expect(resp.response).to.deep.eq({ foo: 'bar' }) + expect(mockCyPrompt.handleBackendRequest).to.be.calledWith('cy:prompt:init', 'foo') - return done() + resolve() + }) }) }) }) @@ -686,7 +686,7 @@ describe('lib/socket', () => { }) context('cy.prompt.addSocketListeners', () => { - it('calls onCyPromptReady with the cy prompt manager', function () { + it('calls onCyPromptReady with the cy prompt manager', async function () { const mockCyPromptManager = { foo: 'bar', } @@ -697,9 +697,7 @@ describe('lib/socket', () => { // Check that the callback was called with the mock cy prompt manager object const registerCyPromptReadyListenerCallback = ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener.firstCall.args[0] - expect(registerCyPromptReadyListenerCallback).to.be.a('function') - - registerCyPromptReadyListenerCallback(mockCyPromptManager) + await registerCyPromptReadyListenerCallback(mockCyPromptManager) expect(this.options.onCyPromptReady).to.be.calledWith(mockCyPromptManager) }) diff --git a/packages/types/src/cy-prompt/index.ts b/packages/types/src/cy-prompt/index.ts index f919c2f2f219..c245ecf71bda 100644 --- a/packages/types/src/cy-prompt/index.ts +++ b/packages/types/src/cy-prompt/index.ts @@ -12,5 +12,5 @@ export interface CyPromptManagerShape extends CyPromptServerShape { export interface CyPromptLifecycleManagerShape { getCyPrompt: () => Promise - registerCyPromptReadyListener: (listener: (cyPromptManager: CyPromptManagerShape) => void) => void + registerCyPromptReadyListener: (listener: (cyPromptManager: CyPromptManagerShape) => Promise) => void } From 2e924d556fa44a0e446980980df67cdacac2d8d2 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 18:09:36 -0500 Subject: [PATCH 04/25] try to fix build --- packages/server/lib/project-base.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index a22fb3a75eee..4f4eab2f86cf 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -515,8 +515,8 @@ export class ProjectBase extends EE { } }, - onCyPromptReady: (cyPromptManager: CyPromptManagerShape) => { - browsers.connectCyPromptToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, cyPromptManager }) + onCyPromptReady: async (cyPromptManager: CyPromptManagerShape) => { + await browsers.connectCyPromptToBrowser({ browser: this.browser, foundBrowsers: this.options.browsers, cyPromptManager }) }, onCaptureVideoFrames: (data: any) => { From 1b2643de2d1893d6e51728e2a124b259e7ec2196 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 18:25:33 -0500 Subject: [PATCH 05/25] Update packages/server/lib/browsers/browser-cri-client.ts Co-authored-by: Bill Glesias --- packages/server/lib/browsers/browser-cri-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 1358b8fb3136..3a992cd72bfc 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -228,7 +228,7 @@ export class BrowserCriClient { * @param options.onReconnect callback for when the browser cri client reconnects to the browser * @param options.port the port to which to connect * @param options.protocolManager the protocol manager to use with the browser cri client - * @param options.cyPromptManager the cy prompt manager to use with the browser cri client + * @param options.cyPromptManager the cy.prompt() manager to use with the browser cri client * @param options.onServiceWorkerClientEvent callback for when a service worker fetch event is received * @returns a wrapper around the chrome remote interface that is connected to the browser target */ From 8c91b7bf0d22f2a0927d7c3fe5cf855ad96faf7d Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Fri, 30 May 2025 18:25:40 -0500 Subject: [PATCH 06/25] Update packages/server/lib/browsers/browser-cri-client.ts Co-authored-by: Bill Glesias --- packages/server/lib/browsers/browser-cri-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 3a992cd72bfc..e8f8c82ab2e0 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -576,7 +576,7 @@ export class BrowserCriClient { await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) } - // Clone the cy prompt target here so that we separate the cy propt client and the main client. + // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. if (!this.currentlyAttachedCyPromptTarget) { this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) From 497e9161be72361e13bc1e668828ffd21238b683 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 16:15:34 -0500 Subject: [PATCH 07/25] do not support prompt in firefox or webkit --- packages/driver/src/cy/commands/prompt/index.ts | 3 ++- .../server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts | 2 +- .../server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts | 4 ++-- .../server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts | 2 +- packages/server/lib/project-base.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 211cfc4f1c23..f76a417a098d 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -40,7 +40,8 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise { - if (Cypress.config('experimentalPromptCommand')) { + if (Cypress.config('experimentalPromptCommand') && + (Cypress.browser.family === 'chrome' || Cypress.browser.name === 'electron')) { Commands.addAll({ async prompt (message: string) { try { diff --git a/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts b/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts index fbdc12171478..0eb3f3879afa 100644 --- a/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts +++ b/packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts @@ -10,7 +10,7 @@ import { verifySignatureFromFile } from '../../encryption' const pkg = require('@packages/root') const _delay = linearDelay(500) -export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: { cyPromptUrl: string, projectId: string, bundlePath: string }) => { +export const getCyPromptBundle = async ({ cyPromptUrl, projectId, bundlePath }: { cyPromptUrl: string, projectId?: string, bundlePath: string }) => { let responseSignature: string | null = null await (asyncRetry(async () => { diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index b4e308e7b320..2b6f8edd4f0e 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -31,7 +31,7 @@ export class CyPromptLifecycleManager { cloudDataSource, ctx, }: { - projectId: string + projectId?: string cloudDataSource: CloudDataSource ctx: DataContext }): void { @@ -89,7 +89,7 @@ export class CyPromptLifecycleManager { projectId, cloudDataSource, }: { - projectId: string + projectId?: string cloudDataSource: CloudDataSource }): Promise { const cyPromptSession = await postCyPromptSession({ diff --git a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts index 14af0eac0e07..5248c479e558 100644 --- a/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts +++ b/packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts @@ -7,7 +7,7 @@ import path from 'path' interface EnsureCyPromptBundleOptions { cyPromptPath: string cyPromptUrl: string - projectId: string + projectId?: string bundlePath: string } diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index 4f4eab2f86cf..c40f8feb6afa 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -157,7 +157,7 @@ export class ProjectBase extends EE { process.chdir(this.projectRoot) this._server = new ServerBase(cfg) - if (cfg.projectId && cfg.experimentalPromptCommand) { + if (cfg.experimentalPromptCommand) { const cyPromptLifecycleManager = new CyPromptLifecycleManager() cyPromptLifecycleManager.initializeCyPromptManager({ From 1a9ea8ab72c220e90f4af6270133c1a47e447418 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Sun, 1 Jun 2025 18:36:14 -0500 Subject: [PATCH 08/25] rework timing of lifecycle --- .../driver/src/cy/commands/prompt/index.ts | 2 +- packages/server/lib/socket-base.ts | 5 +++-- packages/server/test/unit/socket_spec.js | 20 ++----------------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index f76a417a098d..3d709f59618d 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -41,7 +41,7 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise { if (Cypress.config('experimentalPromptCommand') && - (Cypress.browser.family === 'chrome' || Cypress.browser.name === 'electron')) { + (Cypress.browser.family === 'chromium' || Cypress.browser.name === 'electron')) { Commands.addAll({ async prompt (message: string) { try { diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 25c06fc525fd..6a88ce97e7bd 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -452,7 +452,6 @@ export class SocketBase { let cyPrompt: CyPromptManagerShape | undefined getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cp) => { - await options.onCyPromptReady(cp) cyPrompt = cp }) @@ -547,7 +546,9 @@ export class SocketBase { case 'close:extra:targets': return options.closeExtraTargets() case 'wait:for:cy:prompt:ready': - return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then((cyPrompt) => { + return getCtx().coreData.cyPromptLifecycleManager?.getCyPrompt().then(async (cyPrompt) => { + await options.onCyPromptReady(cyPrompt) + return { success: cyPrompt && cyPrompt.status === 'INITIALIZED', } diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index 1cbd507e0113..ec2d3878202b 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -559,6 +559,8 @@ describe('lib/socket', () => { return this.client.emit('backend:request', 'wait:for:cy:prompt:ready', (resp) => { expect(resp.response).to.deep.eq({ success: true }) + expect(this.options.onCyPromptReady).to.be.calledWith(mockCyPrompt) + return done() }) }) @@ -685,24 +687,6 @@ describe('lib/socket', () => { }) }) - context('cy.prompt.addSocketListeners', () => { - it('calls onCyPromptReady with the cy prompt manager', async function () { - const mockCyPromptManager = { - foo: 'bar', - } - - // Verify that registerCyPromptReadyListener was called - expect(ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener).to.be.called - - // Check that the callback was called with the mock cy prompt manager object - const registerCyPromptReadyListenerCallback = ctx.coreData.cyPromptLifecycleManager.registerCyPromptReadyListener.firstCall.args[0] - - await registerCyPromptReadyListenerCallback(mockCyPromptManager) - - expect(this.options.onCyPromptReady).to.be.calledWith(mockCyPromptManager) - }) - }) - context('#isRunnerSocketConnected', function () { it('returns false when runner is not connected', function () { expect(this.socket.isRunnerSocketConnected()).to.eq(false) From 833e267ce9e15307e351fd6deb4f1ec9bf141961 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 09:41:37 -0500 Subject: [PATCH 09/25] refactor --- .../lib/cloud/cy-prompt/CyPromptLifecycleManager.ts | 12 ++++-------- packages/server/lib/routes.ts | 2 +- packages/server/lib/socket-base.ts | 2 +- packages/types/src/cy-prompt/index.ts | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts index 2b6f8edd4f0e..931d709c1ed1 100644 --- a/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts +++ b/packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts @@ -16,8 +16,7 @@ const debug = Debug('cypress:server:cy-prompt-lifecycle-manager') export class CyPromptLifecycleManager { private cyPromptManagerPromise?: Promise private cyPromptManager?: CyPromptManager - private listeners: ((cyPromptManager: CyPromptManager) => Promise)[] = [] - private listenerPromises: Promise[] = [] + private listeners: ((cyPromptManager: CyPromptManager) => void)[] = [] /** * Initialize the cy prompt manager. @@ -134,9 +133,6 @@ export class CyPromptLifecycleManager { this.cyPromptManager = cyPromptManager this.callRegisteredListeners() - await Promise.all(this.listenerPromises) - this.listenerPromises = [] - return cyPromptManager } @@ -149,7 +145,7 @@ export class CyPromptLifecycleManager { debug('Calling all cy prompt ready listeners') this.listeners.forEach((listener) => { - this.listenerPromises.push(listener(cyPromptManager)) + listener(cyPromptManager) }) this.listeners = [] @@ -159,11 +155,11 @@ export class CyPromptLifecycleManager { * Register a listener that will be called when the cy prompt manager is ready * @param listener Function to call when cy prompt manager is ready */ - registerCyPromptReadyListener (listener: (cyPromptManager: CyPromptManager) => Promise): void { + registerCyPromptReadyListener (listener: (cyPromptManager: CyPromptManager) => void): void { // if there is already a cy prompt manager, call the listener immediately if (this.cyPromptManager) { debug('cy prompt ready - calling listener immediately') - this.listenerPromises.push(listener(this.cyPromptManager)) + listener(this.cyPromptManager) } else { debug('cy prompt not ready - registering cy prompt ready listener') this.listeners.push(listener) diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index fff37f2282fc..1c7f9fdfd487 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -127,7 +127,7 @@ export const createCommonRoutes = ({ const cyPromptRouter = Router() router.use('/', cyPromptRouter) - getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cyPrompt) => { + getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cyPrompt) => { cyPrompt.initializeRoutes(cyPromptRouter) }) } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 6a88ce97e7bd..aa254b2e4285 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -451,7 +451,7 @@ export class SocketBase { let cyPrompt: CyPromptManagerShape | undefined - getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener(async (cp) => { + getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cp) => { cyPrompt = cp }) diff --git a/packages/types/src/cy-prompt/index.ts b/packages/types/src/cy-prompt/index.ts index c245ecf71bda..f919c2f2f219 100644 --- a/packages/types/src/cy-prompt/index.ts +++ b/packages/types/src/cy-prompt/index.ts @@ -12,5 +12,5 @@ export interface CyPromptManagerShape extends CyPromptServerShape { export interface CyPromptLifecycleManagerShape { getCyPrompt: () => Promise - registerCyPromptReadyListener: (listener: (cyPromptManager: CyPromptManagerShape) => Promise) => void + registerCyPromptReadyListener: (listener: (cyPromptManager: CyPromptManagerShape) => void) => void } From 24ae06898b8ce9b673ca77bd90b618c4b9cfbf6c Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 11:36:12 -0500 Subject: [PATCH 10/25] fix tests --- packages/driver/cypress/e2e/commands/prompt.cy.ts | 5 +++++ packages/driver/src/cy/commands/prompt/index.ts | 8 ++++++-- packages/server/test/unit/project_spec.js | 12 ------------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/prompt.cy.ts b/packages/driver/cypress/e2e/commands/prompt.cy.ts index 9fb327349909..254d06a37474 100644 --- a/packages/driver/cypress/e2e/commands/prompt.cy.ts +++ b/packages/driver/cypress/e2e/commands/prompt.cy.ts @@ -1,5 +1,10 @@ describe('src/cy/commands/prompt', () => { it('executes the prompt command', () => { + // TODO: test the error messages + if (Cypress.isBrowser('webkit') || Cypress.isBrowser('firefox')) { + return + } + cy.visit('/fixtures/dom.html') // TODO: add more tests when cy.prompt is built out, but for now this just diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 3d709f59618d..2522f101160f 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -40,10 +40,14 @@ const initializeCloudCyPrompt = async (Cypress: Cypress.Cypress): Promise { - if (Cypress.config('experimentalPromptCommand') && - (Cypress.browser.family === 'chromium' || Cypress.browser.name === 'electron')) { + if (Cypress.config('experimentalPromptCommand')) { Commands.addAll({ async prompt (message: string) { + if (Cypress.browser.family !== 'chromium' && Cypress.browser.name !== 'electron') { + // TODO: handle this better + throw new Error('`cy.prompt()` is not supported in this browser.') + } + try { let cloud = initializedCyPrompt diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index abb6591f5d90..8f3477b3365d 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -477,18 +477,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s expect(CyPromptLifecycleManager.prototype.initializeCyPromptManager).not.to.be.called }) }) - - it('does not initialize cy prompt lifecycle manager if projectId is not set', function () { - this.config.projectId = undefined - this.config.experimentalPromptCommand = true - - sinon.stub(CyPromptLifecycleManager.prototype, 'initializeCyPromptManager') - - return this.project.open() - .then(() => { - expect(CyPromptLifecycleManager.prototype.initializeCyPromptManager).not.to.be.called - }) - }) }) describe('saved state', function () { From e341257afa6096356bd532cfd8c874bfd31b8475 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 12:59:54 -0500 Subject: [PATCH 11/25] troubleshooting --- packages/server/lib/socket-base.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index aa254b2e4285..5fcaceaf4766 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -465,7 +465,20 @@ export class SocketBase { const backendRequest = () => { if (eventName.startsWith('cy:prompt:')) { - return cyPrompt?.handleBackendRequest(eventName, ...args) + try { + // eslint-disable-next-line no-console + console.log('cyPrompt', eventName, ...args) + + return cyPrompt?.handleBackendRequest(eventName, ...args).then((result) => { + // eslint-disable-next-line no-console + console.log('result', result) + + return result + }) + } catch (error) { + // eslint-disable-next-line no-console + console.error('error', error) + } } switch (eventName) { From 54d2953f60b369f82ac468d3866f5f8a4b0a5e2c Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 13:23:22 -0500 Subject: [PATCH 12/25] troubleshooting --- packages/server/lib/socket-base.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 5fcaceaf4766..c78f3e1cb481 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -465,20 +465,20 @@ export class SocketBase { const backendRequest = () => { if (eventName.startsWith('cy:prompt:')) { - try { - // eslint-disable-next-line no-console - console.log('cyPrompt', eventName, ...args) + // eslint-disable-next-line no-console + console.log('cyPrompt', eventName, ...args) - return cyPrompt?.handleBackendRequest(eventName, ...args).then((result) => { - // eslint-disable-next-line no-console - console.log('result', result) + return cyPrompt?.handleBackendRequest(eventName, ...args).then((result) => { + // eslint-disable-next-line no-console + console.log('result', result) - return result - }) - } catch (error) { + return result + }).catch((error) => { // eslint-disable-next-line no-console console.error('error', error) - } + + return error + }) } switch (eventName) { From ffed28666f3f20720dcdbb7de45ca2ac34574d35 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 14:17:19 -0500 Subject: [PATCH 13/25] fix tests --- .../server/lib/browsers/browser-cri-client.ts | 15 ++++-- .../unit/browsers/browser-cri-client_spec.ts | 48 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index e8f8c82ab2e0..0579c20b9ad7 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -475,7 +475,8 @@ export class BrowserCriClient { // always close the connection to the page target because it was destroyed browserCriClient.currentlyAttachedTarget.close().catch(() => { }), - browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => {}) + browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => {}), + browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => {}) new Bluebird((resolve) => { // this event could fire either expectedly or unexpectedly @@ -622,6 +623,7 @@ export class BrowserCriClient { await this.currentlyAttachedTarget.close().catch(() => {}) await this.currentlyAttachedProtocolTarget?.close().catch(() => {}) + await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) debug('target client closed', this.currentlyAttachedTarget.targetId) } @@ -634,6 +636,10 @@ export class BrowserCriClient { this.browserClient.off(subscription.eventName, subscription.cb as any) }) + this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { + this.browserClient.off(subscription.eventName, subscription.cb as any) + }) + if (target) { this.currentlyAttachedTarget = await CriClient.create({ target: target.targetId, @@ -645,13 +651,15 @@ export class BrowserCriClient { browserClient: this.browserClient, }) - // Clone the target here so that we separate the protocol client and the main client. - // This allows us to close the protocol client independently of the main client + // Clone the targets here so that we separate these clients from the main client. + // This allows us to close these clients independently of the main client // which we do when we exit out of studio in open mode. this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() + this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined + this.currentlyAttachedCyPromptTarget = undefined } this.resettingBrowserTargets = false @@ -712,6 +720,7 @@ export class BrowserCriClient { if (this.currentlyAttachedTarget) { await this.currentlyAttachedTarget.close() await this.currentlyAttachedProtocolTarget?.close() + await this.currentlyAttachedCyPromptTarget?.close() } await this.browserClient.close() diff --git a/packages/server/test/unit/browsers/browser-cri-client_spec.ts b/packages/server/test/unit/browsers/browser-cri-client_spec.ts index 7258149346fc..b751ec450ad7 100644 --- a/packages/server/test/unit/browsers/browser-cri-client_spec.ts +++ b/packages/server/test/unit/browsers/browser-cri-client_spec.ts @@ -371,6 +371,9 @@ describe('lib/browsers/browser-cri-client', function () { currentlyAttachedProtocolTarget: { close: sinon.stub().resolves(), }, + currentlyAttachedCyPromptTarget: { + close: sinon.stub().resolves(), + }, resettingBrowserTargets: false, }, event: { @@ -387,6 +390,7 @@ describe('lib/browsers/browser-cri-client', function () { expect(options.browserCriClient.getExtraTargetClient).not.to.be.called expect(options.browserCriClient.currentlyAttachedTarget.close).not.to.be.called expect(options.browserCriClient.currentlyAttachedProtocolTarget.close).not.to.be.called + expect(options.browserCriClient.currentlyAttachedCyPromptTarget.close).not.to.be.called }) it('closes the extra target client', () => { @@ -597,13 +601,28 @@ describe('lib/browsers/browser-cri-client', function () { }, } + const mockCurrentlyAttachedCyPromptTarget = { + targetId: '100', + close: sinon.stub().resolves(sinon.stub().resolves()), + queue: { + subscriptions: [{ + eventName: 'Network.requestWillBeSent', + cb: sinon.stub(), + }], + }, + } + const mockUpdatedCurrentlyAttachedProtocolTarget = { targetId: '101', } + const mockUpdatedCurrentlyAttachedCyPromptTarget = { + targetId: '101', + } + const mockUpdatedCurrentlyAttachedTarget = { targetId: '101', - clone: sinon.stub().returns(mockUpdatedCurrentlyAttachedProtocolTarget), + clone: sinon.stub().onFirstCall().returns(mockUpdatedCurrentlyAttachedProtocolTarget).onSecondCall().returns(mockUpdatedCurrentlyAttachedCyPromptTarget), } send.withArgs('Target.createTarget', { url: 'about:blank' }).resolves(mockUpdatedCurrentlyAttachedTarget) @@ -615,6 +634,7 @@ describe('lib/browsers/browser-cri-client', function () { browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget + browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget browserClient.browserClient.off = sinon.stub() await browserClient.resetBrowserTargets(true) @@ -622,8 +642,10 @@ describe('lib/browsers/browser-cri-client', function () { expect(mockCurrentlyAttachedTarget.close).to.be.called expect(browserClient.currentlyAttachedTarget).to.eql(mockUpdatedCurrentlyAttachedTarget) expect(browserClient.currentlyAttachedProtocolTarget).to.eql(mockUpdatedCurrentlyAttachedProtocolTarget) + expect(browserClient.currentlyAttachedCyPromptTarget).to.eql(mockUpdatedCurrentlyAttachedCyPromptTarget) expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedTarget.queue.subscriptions[0].cb) expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedProtocolTarget.queue.subscriptions[0].cb) + expect(browserClient.browserClient.off).to.be.calledWith('Network.requestWillBeSent', mockCurrentlyAttachedCyPromptTarget.queue.subscriptions[0].cb) }) it('closes the currently attached target without keeping a tab open', async function () { @@ -643,19 +665,30 @@ describe('lib/browsers/browser-cri-client', function () { }, } + const mockCurrentlyAttachedCyPromptTarget = { + targetId: '100', + close: sinon.stub().resolves(sinon.stub().resolves()), + queue: { + subscriptions: [], + }, + } + send.withArgs('Target.closeTarget', { targetId: '100' }).resolves() const browserClient = await getClient() as any browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget + browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget await browserClient.resetBrowserTargets(false) expect(mockCurrentlyAttachedTarget.close).to.be.called expect(mockCurrentlyAttachedProtocolTarget.close).to.be.called + expect(mockCurrentlyAttachedCyPromptTarget.close).to.be.called expect(browserClient.currentlyAttachedTarget).to.be.undefined expect(browserClient.currentlyAttachedProtocolTarget).to.be.undefined + expect(browserClient.currentlyAttachedCyPromptTarget).to.be.undefined }) it('throws when there is no currently attached target', async function () { @@ -703,14 +736,25 @@ describe('lib/browsers/browser-cri-client', function () { close: sinon.stub().resolves(), } + const mockCurrentlyAttachedProtocolTarget = { + close: sinon.stub().resolves(), + } + + const mockCurrentlyAttachedCyPromptTarget = { + close: sinon.stub().resolves(), + } + const browserClient = await getClient() as any browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget + browserClient.currentlyAttachedProtocolTarget = mockCurrentlyAttachedProtocolTarget + browserClient.currentlyAttachedCyPromptTarget = mockCurrentlyAttachedCyPromptTarget await browserClient.close() expect(mockCurrentlyAttachedTarget.close).to.be.called - expect(close).to.be.called + expect(mockCurrentlyAttachedProtocolTarget.close).to.be.called + expect(mockCurrentlyAttachedCyPromptTarget.close).to.be.called }) it('just the browser client with no currently attached target', async function () { From a7f0c8c031027e370c42008e5b3d7d812b7152eb Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 16:16:00 -0500 Subject: [PATCH 14/25] additional troubleshooting --- .../server/lib/browsers/browser-cri-client.ts | 30 +++++++++++++++---- system-tests/test/visit_spec.js | 2 +- .../cache/darwin/snapshot-meta.json | 11 +++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 0579c20b9ad7..467942aa90d6 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -474,9 +474,13 @@ export class BrowserCriClient { // otherwise it means the the browser itself was closed // always close the connection to the page target because it was destroyed - browserCriClient.currentlyAttachedTarget.close().catch(() => { }), - browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => {}), - browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => {}) + browserCriClient.currentlyAttachedTarget.close().catch(() => { }) + browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { }) + // eslint-disable-next-line no-console + console.log('closing cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget) + browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) + // eslint-disable-next-line no-console + console.log('closed cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget) new Bluebird((resolve) => { // this event could fire either expectedly or unexpectedly @@ -623,7 +627,11 @@ export class BrowserCriClient { await this.currentlyAttachedTarget.close().catch(() => {}) await this.currentlyAttachedProtocolTarget?.close().catch(() => {}) + // eslint-disable-next-line no-console + console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget) await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) + // eslint-disable-next-line no-console + console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget) debug('target client closed', this.currentlyAttachedTarget.targetId) } @@ -636,10 +644,15 @@ export class BrowserCriClient { this.browserClient.off(subscription.eventName, subscription.cb as any) }) + // eslint-disable-next-line no-console + console.log('turning off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget) this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { this.browserClient.off(subscription.eventName, subscription.cb as any) }) + // eslint-disable-next-line no-console + console.log('turned off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget) + if (target) { this.currentlyAttachedTarget = await CriClient.create({ target: target.targetId, @@ -652,10 +665,13 @@ export class BrowserCriClient { }) // Clone the targets here so that we separate these clients from the main client. - // This allows us to close these clients independently of the main client - // which we do when we exit out of studio in open mode. + // This allows us to operate these clients independently of the main client this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() + // eslint-disable-next-line no-console + console.log('cloned protocol target') this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() + // eslint-disable-next-line no-console + console.log('cloned cy prompt target', this.currentlyAttachedCyPromptTarget) } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined @@ -720,7 +736,11 @@ export class BrowserCriClient { if (this.currentlyAttachedTarget) { await this.currentlyAttachedTarget.close() await this.currentlyAttachedProtocolTarget?.close() + // eslint-disable-next-line no-console + console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget) await this.currentlyAttachedCyPromptTarget?.close() + // eslint-disable-next-line no-console + console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget) } await this.browserClient.close() diff --git a/system-tests/test/visit_spec.js b/system-tests/test/visit_spec.js index 43cbcd1c2a48..5b0a81e9773a 100644 --- a/system-tests/test/visit_spec.js +++ b/system-tests/test/visit_spec.js @@ -145,7 +145,7 @@ describe('e2e visit', () => { }, }) - systemTests.it('passes with experimentalSourceRewriting', { + systemTests.it.only('passes with experimentalSourceRewriting', { browser: '!webkit', // TODO(webkit): fix+unskip spec: 'source_rewriting.cy.js', config: { diff --git a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json index 39dd8c01fafb..49e2fbfc1cb3 100644 --- a/tooling/v8-snapshot/cache/darwin/snapshot-meta.json +++ b/tooling/v8-snapshot/cache/darwin/snapshot-meta.json @@ -1052,6 +1052,8 @@ "./node_modules/@cypress/commit-info/src/index.js", "./node_modules/@cypress/commit-info/src/utils.js", "./node_modules/@cypress/get-windows-proxy/node_modules/debug/src/common.js", + "./node_modules/@cypress/get-windows-proxy/node_modules/registry-js/dist/lib/index.js", + "./node_modules/@cypress/get-windows-proxy/node_modules/registry-js/dist/lib/registry.js", "./node_modules/@cypress/get-windows-proxy/src/index.js", "./node_modules/@cypress/parse-domain/build/tries/icann.complete.json", "./node_modules/@cypress/parse-domain/build/tries/private.complete.json", @@ -3183,6 +3185,8 @@ "./node_modules/recast/parsers/babel.js", "./node_modules/recast/parsers/esprima.js", "./node_modules/recast/parsers/typescript.js", + "./node_modules/registry-js/dist/lib/index.js", + "./node_modules/registry-js/dist/lib/registry.js", "./node_modules/request-promise-core/configure/request2.js", "./node_modules/request-promise-core/errors.js", "./node_modules/request-promise-core/lib/errors.js", @@ -3854,6 +3858,8 @@ "./packages/server/lib/browsers/webkit.ts", "./packages/server/lib/cloud/api/axios_middleware/logging.ts", "./packages/server/lib/cloud/api/axios_middleware/transform_error.ts", + "./packages/server/lib/cloud/api/cy-prompt/get_cy_prompt_bundle.ts", + "./packages/server/lib/cloud/api/cy-prompt/post_cy_prompt_session.ts", "./packages/server/lib/cloud/api/scrub_url.ts", "./packages/server/lib/cloud/api/studio/get_studio_bundle.ts", "./packages/server/lib/cloud/api/studio/post_studio_session.ts", @@ -3863,6 +3869,9 @@ "./packages/server/lib/cloud/artifacts/print_protocol_upload_error.ts", "./packages/server/lib/cloud/artifacts/upload_artifacts.ts", "./packages/server/lib/cloud/constants.ts", + "./packages/server/lib/cloud/cy-prompt/CyPromptLifecycleManager.ts", + "./packages/server/lib/cloud/cy-prompt/CyPromptManager.ts", + "./packages/server/lib/cloud/cy-prompt/ensure_cy_prompt_bundle.ts", "./packages/server/lib/cloud/encryption.ts", "./packages/server/lib/cloud/environment.ts", "./packages/server/lib/cloud/get_cloud_metadata.ts", @@ -4167,6 +4176,8 @@ "./packages/types/src/cloud.ts", "./packages/types/src/config.ts", "./packages/types/src/constants.ts", + "./packages/types/src/cy-prompt/cy-prompt-server-types.ts", + "./packages/types/src/cy-prompt/index.ts", "./packages/types/src/driver.ts", "./packages/types/src/editors.ts", "./packages/types/src/git.ts", From 3bd37d0faaf72a2fde779a4fa473113bb7e55cc5 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 16:34:29 -0500 Subject: [PATCH 15/25] additional troubleshooting --- .../server/lib/browsers/browser-cri-client.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 467942aa90d6..98541d362405 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -477,10 +477,10 @@ export class BrowserCriClient { browserCriClient.currentlyAttachedTarget.close().catch(() => { }) browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { }) // eslint-disable-next-line no-console - console.log('closing cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget) + console.log('closing cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget?.targetId) browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) // eslint-disable-next-line no-console - console.log('closed cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget) + console.log('closed cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget?.targetId) new Bluebird((resolve) => { // this event could fire either expectedly or unexpectedly @@ -628,10 +628,10 @@ export class BrowserCriClient { await this.currentlyAttachedTarget.close().catch(() => {}) await this.currentlyAttachedProtocolTarget?.close().catch(() => {}) // eslint-disable-next-line no-console - console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget) + console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) // eslint-disable-next-line no-console - console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget) + console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) debug('target client closed', this.currentlyAttachedTarget.targetId) } @@ -645,13 +645,13 @@ export class BrowserCriClient { }) // eslint-disable-next-line no-console - console.log('turning off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget) + console.log('turning off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget?.targetId) this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { this.browserClient.off(subscription.eventName, subscription.cb as any) }) // eslint-disable-next-line no-console - console.log('turned off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget) + console.log('turned off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget?.targetId) if (target) { this.currentlyAttachedTarget = await CriClient.create({ @@ -671,7 +671,7 @@ export class BrowserCriClient { console.log('cloned protocol target') this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() // eslint-disable-next-line no-console - console.log('cloned cy prompt target', this.currentlyAttachedCyPromptTarget) + console.log('cloned cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined @@ -737,10 +737,10 @@ export class BrowserCriClient { await this.currentlyAttachedTarget.close() await this.currentlyAttachedProtocolTarget?.close() // eslint-disable-next-line no-console - console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget) + console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) await this.currentlyAttachedCyPromptTarget?.close() // eslint-disable-next-line no-console - console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget) + console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) } await this.browserClient.close() From 66a5dcf1d78c5c7a7a14e89a179c3b06dac5cb4d Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 16:50:30 -0500 Subject: [PATCH 16/25] additional troubleshooting --- system-tests/test/visit_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-tests/test/visit_spec.js b/system-tests/test/visit_spec.js index 5b0a81e9773a..43cbcd1c2a48 100644 --- a/system-tests/test/visit_spec.js +++ b/system-tests/test/visit_spec.js @@ -145,7 +145,7 @@ describe('e2e visit', () => { }, }) - systemTests.it.only('passes with experimentalSourceRewriting', { + systemTests.it('passes with experimentalSourceRewriting', { browser: '!webkit', // TODO(webkit): fix+unskip spec: 'source_rewriting.cy.js', config: { From 2445b7c463d79c1d85eb02d0673677d1d87a8b9c Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 19:23:46 -0500 Subject: [PATCH 17/25] attempt to fix build --- .../server/lib/browsers/browser-cri-client.ts | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 98541d362405..81a2aae3bf48 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -476,11 +476,7 @@ export class BrowserCriClient { // always close the connection to the page target because it was destroyed browserCriClient.currentlyAttachedTarget.close().catch(() => { }) browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { }) - // eslint-disable-next-line no-console - console.log('closing cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget?.targetId) - browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) - // eslint-disable-next-line no-console - console.log('closed cy prompt target', browserCriClient.currentlyAttachedCyPromptTarget?.targetId) + // browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) new Bluebird((resolve) => { // this event could fire either expectedly or unexpectedly @@ -627,11 +623,7 @@ export class BrowserCriClient { await this.currentlyAttachedTarget.close().catch(() => {}) await this.currentlyAttachedProtocolTarget?.close().catch(() => {}) - // eslint-disable-next-line no-console - console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) - await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) - // eslint-disable-next-line no-console - console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) + // await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) debug('target client closed', this.currentlyAttachedTarget.targetId) } @@ -644,14 +636,9 @@ export class BrowserCriClient { this.browserClient.off(subscription.eventName, subscription.cb as any) }) - // eslint-disable-next-line no-console - console.log('turning off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget?.targetId) - this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { - this.browserClient.off(subscription.eventName, subscription.cb as any) - }) - - // eslint-disable-next-line no-console - console.log('turned off cy prompt target subscriptions', this.currentlyAttachedCyPromptTarget?.targetId) + // this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { + // this.browserClient.off(subscription.eventName, subscription.cb as any) + // }) if (target) { this.currentlyAttachedTarget = await CriClient.create({ @@ -667,11 +654,7 @@ export class BrowserCriClient { // Clone the targets here so that we separate these clients from the main client. // This allows us to operate these clients independently of the main client this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() - // eslint-disable-next-line no-console - console.log('cloned protocol target') - this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() - // eslint-disable-next-line no-console - console.log('cloned cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) + // this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined @@ -736,11 +719,7 @@ export class BrowserCriClient { if (this.currentlyAttachedTarget) { await this.currentlyAttachedTarget.close() await this.currentlyAttachedProtocolTarget?.close() - // eslint-disable-next-line no-console - console.log('closing cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) - await this.currentlyAttachedCyPromptTarget?.close() - // eslint-disable-next-line no-console - console.log('closed cy prompt target', this.currentlyAttachedCyPromptTarget?.targetId) + // await this.currentlyAttachedCyPromptTarget?.close() } await this.browserClient.close() From b7cd38ec89ca4893113851db92fd6a1fb27fd345 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 19:42:01 -0500 Subject: [PATCH 18/25] add back --- packages/server/lib/browsers/browser-cri-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 81a2aae3bf48..80bc0c5d5873 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -719,7 +719,7 @@ export class BrowserCriClient { if (this.currentlyAttachedTarget) { await this.currentlyAttachedTarget.close() await this.currentlyAttachedProtocolTarget?.close() - // await this.currentlyAttachedCyPromptTarget?.close() + await this.currentlyAttachedCyPromptTarget?.close() } await this.browserClient.close() From deef67a83513b9789d0d88049c2268806cc39914 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 20:17:11 -0500 Subject: [PATCH 19/25] debugging --- system-tests/test/visit_spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system-tests/test/visit_spec.js b/system-tests/test/visit_spec.js index 43cbcd1c2a48..e6aa4cfc05e5 100644 --- a/system-tests/test/visit_spec.js +++ b/system-tests/test/visit_spec.js @@ -151,6 +151,9 @@ describe('e2e visit', () => { config: { experimentalSourceRewriting: true, }, + env: { + DEBUG: 'cypress:server:browsers:cri-client', + }, snapshot: true, onRun (exec) { return startTlsV1Server(6776) From c006eabaa2cc5165aed0264343d83166bc214cba Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 20:36:39 -0500 Subject: [PATCH 20/25] debugging --- system-tests/test/visit_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system-tests/test/visit_spec.js b/system-tests/test/visit_spec.js index e6aa4cfc05e5..9cf705507be4 100644 --- a/system-tests/test/visit_spec.js +++ b/system-tests/test/visit_spec.js @@ -151,7 +151,7 @@ describe('e2e visit', () => { config: { experimentalSourceRewriting: true, }, - env: { + processEnv: { DEBUG: 'cypress:server:browsers:cri-client', }, snapshot: true, From ae144de45111b4010c0ab1874e0a2baa61e3d6ed Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 21:19:00 -0500 Subject: [PATCH 21/25] debugging --- packages/server/lib/socket-base.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index c78f3e1cb481..0f092c900b23 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -694,12 +694,6 @@ export class SocketBase { end () { this.ended = true - - // TODO: we need an 'ack' from this end - // event from the other side - this.getIos().forEach((io) => { - io?.emit('tests:finished') - }) } async resetBrowserTabsForNextSpec (shouldKeepTabOpen: boolean) { From 5b85e00a8b6e109bc627b4e8596e70e7b75bc4ce Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 2 Jun 2025 22:13:10 -0500 Subject: [PATCH 22/25] debugging --- .../server/lib/browsers/browser-cri-client.ts | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 80bc0c5d5873..6aaa40e7d581 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -476,7 +476,7 @@ export class BrowserCriClient { // always close the connection to the page target because it was destroyed browserCriClient.currentlyAttachedTarget.close().catch(() => { }) browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { }) - // browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) + browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { }) new Bluebird((resolve) => { // this event could fire either expectedly or unexpectedly @@ -621,9 +621,11 @@ export class BrowserCriClient { debug('target closed', this.currentlyAttachedTarget.targetId) - await this.currentlyAttachedTarget.close().catch(() => {}) - await this.currentlyAttachedProtocolTarget?.close().catch(() => {}) - // await this.currentlyAttachedCyPromptTarget?.close().catch(() => {}) + await Promise.all([ + this.currentlyAttachedTarget.close().catch(() => {}), + this.currentlyAttachedProtocolTarget?.close().catch(() => {}), + this.currentlyAttachedCyPromptTarget?.close().catch(() => {}), + ]) debug('target client closed', this.currentlyAttachedTarget.targetId) } @@ -636,9 +638,9 @@ export class BrowserCriClient { this.browserClient.off(subscription.eventName, subscription.cb as any) }) - // this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { - // this.browserClient.off(subscription.eventName, subscription.cb as any) - // }) + this.currentlyAttachedCyPromptTarget?.queue.subscriptions.forEach((subscription) => { + this.browserClient.off(subscription.eventName, subscription.cb as any) + }) if (target) { this.currentlyAttachedTarget = await CriClient.create({ @@ -654,7 +656,7 @@ export class BrowserCriClient { // Clone the targets here so that we separate these clients from the main client. // This allows us to operate these clients independently of the main client this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() - // this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() + this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined @@ -717,9 +719,11 @@ export class BrowserCriClient { this.connected = false if (this.currentlyAttachedTarget) { - await this.currentlyAttachedTarget.close() - await this.currentlyAttachedProtocolTarget?.close() - await this.currentlyAttachedCyPromptTarget?.close() + await Promise.all([ + this.currentlyAttachedTarget.close(), + this.currentlyAttachedProtocolTarget?.close(), + this.currentlyAttachedCyPromptTarget?.close(), + ]) } await this.browserClient.close() From b64b9d9726fbd59cd4688f292d5663b315e7b5c8 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 08:01:09 -0500 Subject: [PATCH 23/25] clean up --- .../driver/cypress/e2e/commands/prompt.cy.ts | 4 +- .../driver/src/cy/commands/prompt/index.ts | 3 +- .../server/lib/browsers/browser-cri-client.ts | 43 +++++++++++++------ packages/server/lib/socket-base.ts | 21 +++------ .../cloud/cy-prompt/CyPromptManager_spec.ts | 4 +- system-tests/test/visit_spec.js | 3 -- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/driver/cypress/e2e/commands/prompt.cy.ts b/packages/driver/cypress/e2e/commands/prompt.cy.ts index 254d06a37474..78ba0620c4bc 100644 --- a/packages/driver/cypress/e2e/commands/prompt.cy.ts +++ b/packages/driver/cypress/e2e/commands/prompt.cy.ts @@ -1,6 +1,8 @@ describe('src/cy/commands/prompt', () => { it('executes the prompt command', () => { - // TODO: test the error messages + // TODO: (cy.prompt) We will look into supporting other browsers + // as this is rolled out. We will add error messages for other browsers + // and add tests if necessary if (Cypress.isBrowser('webkit') || Cypress.isBrowser('firefox')) { return } diff --git a/packages/driver/src/cy/commands/prompt/index.ts b/packages/driver/src/cy/commands/prompt/index.ts index 2522f101160f..96d00215a12e 100644 --- a/packages/driver/src/cy/commands/prompt/index.ts +++ b/packages/driver/src/cy/commands/prompt/index.ts @@ -44,7 +44,8 @@ export default (Commands, Cypress, cy) => { Commands.addAll({ async prompt (message: string) { if (Cypress.browser.family !== 'chromium' && Cypress.browser.name !== 'electron') { - // TODO: handle this better + // 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.') } diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 6aaa40e7d581..35884a8fd285 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -569,19 +569,26 @@ export class BrowserCriClient { browserClient: this.browserClient, }) - // Clone the target here so that we separate the protocol client and the main client. - // This allows us to close the protocol client independently of the main client - // which we do when we exit out of studio in open mode. - if (!this.currentlyAttachedProtocolTarget) { - this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() - await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) - } + const currentTarget = this.currentlyAttachedTarget - // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. - if (!this.currentlyAttachedCyPromptTarget) { - this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() - await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) - } + await Promise.all([ + async () => { + // Clone the target here so that we separate the protocol client and the main client. + // This allows us to close the protocol client independently of the main client + // which we do when we exit out of studio in open mode. + if (!this.currentlyAttachedProtocolTarget) { + this.currentlyAttachedProtocolTarget = await currentTarget.clone() + await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) + } + }, + async () => { + // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. + if (!this.currentlyAttachedCyPromptTarget) { + this.currentlyAttachedCyPromptTarget = await currentTarget.clone() + await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) + } + }, + ]) return this.currentlyAttachedTarget }, this.browserName, this.port) @@ -653,10 +660,18 @@ export class BrowserCriClient { browserClient: this.browserClient, }) + const currentTarget = this.currentlyAttachedTarget + // Clone the targets here so that we separate these clients from the main client. // This allows us to operate these clients independently of the main client - this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() - this.currentlyAttachedCyPromptTarget = await this.currentlyAttachedTarget.clone() + await Promise.all([ + async () => { + this.currentlyAttachedProtocolTarget = await currentTarget.clone() + }, + async () => { + this.currentlyAttachedCyPromptTarget = await currentTarget.clone() + }, + ]) } else { this.currentlyAttachedTarget = undefined this.currentlyAttachedProtocolTarget = undefined diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index 0f092c900b23..aa254b2e4285 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -465,20 +465,7 @@ export class SocketBase { const backendRequest = () => { if (eventName.startsWith('cy:prompt:')) { - // eslint-disable-next-line no-console - console.log('cyPrompt', eventName, ...args) - - return cyPrompt?.handleBackendRequest(eventName, ...args).then((result) => { - // eslint-disable-next-line no-console - console.log('result', result) - - return result - }).catch((error) => { - // eslint-disable-next-line no-console - console.error('error', error) - - return error - }) + return cyPrompt?.handleBackendRequest(eventName, ...args) } switch (eventName) { @@ -694,6 +681,12 @@ export class SocketBase { end () { this.ended = true + + // TODO: we need an 'ack' from this end + // event from the other side + this.getIos().forEach((io) => { + io?.emit('tests:finished') + }) } async resetBrowserTabsForNextSpec (shouldKeepTabOpen: boolean) { 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 905f8c73d4aa..bbf19ce91af1 100644 --- a/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts +++ b/packages/server/test/unit/cloud/cy-prompt/CyPromptManager_spec.ts @@ -53,7 +53,7 @@ describe('lib/cloud/cy-prompt', () => { expect(cyPromptManager.status).to.eq('IN_ERROR') - // TODO: test that the error is reported + // TODO: (cy.prompt) test that the error is reported }) }) @@ -65,7 +65,7 @@ describe('lib/cloud/cy-prompt', () => { await cyPromptManager.handleBackendRequest('cy:prompt:start', {} as any) - // TODO: test that the error is reported + // TODO: (cy.prompt) test that the error is reported }) }) diff --git a/system-tests/test/visit_spec.js b/system-tests/test/visit_spec.js index 9cf705507be4..43cbcd1c2a48 100644 --- a/system-tests/test/visit_spec.js +++ b/system-tests/test/visit_spec.js @@ -151,9 +151,6 @@ describe('e2e visit', () => { config: { experimentalSourceRewriting: true, }, - processEnv: { - DEBUG: 'cypress:server:browsers:cri-client', - }, snapshot: true, onRun (exec) { return startTlsV1Server(6776) From 3b21906efd9237a86d72d9f8a5801d351b88c38e Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 08:28:43 -0500 Subject: [PATCH 24/25] fix unit tests --- .../server/lib/browsers/browser-cri-client.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 35884a8fd285..11681c8024f4 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -571,23 +571,29 @@ export class BrowserCriClient { const currentTarget = this.currentlyAttachedTarget + const createProtocolTarget = async () => { + // Clone the target here so that we separate the protocol client and the main client. + // This allows us to close the protocol client independently of the main client + // which we do when we exit out of studio in open mode. + if (!this.currentlyAttachedProtocolTarget) { + this.currentlyAttachedProtocolTarget = await currentTarget.clone() + } + + await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) + } + + const createCyPromptTarget = async () => { + // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. + if (!this.currentlyAttachedCyPromptTarget) { + this.currentlyAttachedCyPromptTarget = await currentTarget.clone() + } + + await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) + } + await Promise.all([ - async () => { - // Clone the target here so that we separate the protocol client and the main client. - // This allows us to close the protocol client independently of the main client - // which we do when we exit out of studio in open mode. - if (!this.currentlyAttachedProtocolTarget) { - this.currentlyAttachedProtocolTarget = await currentTarget.clone() - await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) - } - }, - async () => { - // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. - if (!this.currentlyAttachedCyPromptTarget) { - this.currentlyAttachedCyPromptTarget = await currentTarget.clone() - await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) - } - }, + createProtocolTarget(), + createCyPromptTarget(), ]) return this.currentlyAttachedTarget @@ -662,15 +668,17 @@ export class BrowserCriClient { const currentTarget = this.currentlyAttachedTarget - // Clone the targets here so that we separate these clients from the main client. - // This allows us to operate these clients independently of the main client + const createProtocolTarget = async () => { + this.currentlyAttachedProtocolTarget = await currentTarget.clone() + } + + const createCyPromptTarget = async () => { + this.currentlyAttachedCyPromptTarget = await currentTarget.clone() + } + await Promise.all([ - async () => { - this.currentlyAttachedProtocolTarget = await currentTarget.clone() - }, - async () => { - this.currentlyAttachedCyPromptTarget = await currentTarget.clone() - }, + createProtocolTarget(), + createCyPromptTarget(), ]) } else { this.currentlyAttachedTarget = undefined From 335148973a21793dda978e953afac669f7be5c21 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Tue, 3 Jun 2025 09:17:26 -0500 Subject: [PATCH 25/25] rework --- .../server/lib/browsers/browser-cri-client.ts | 35 ++++--------------- .../unit/browsers/browser-cri-client_spec.ts | 28 ++++----------- 2 files changed, 13 insertions(+), 50 deletions(-) diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 11681c8024f4..db53b515950e 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -26,7 +26,6 @@ type BrowserCriClientOptions = { browserName: string onAsynchronousError: (err: CypressError) => void protocolManager?: ProtocolManagerShape - cyPromptManager?: CyPromptManagerShape fullyManageTabs?: boolean onServiceWorkerClientEvent: ServiceWorkerEventHandler } @@ -211,7 +210,6 @@ export class BrowserCriClient { this.browserName = options.browserName this.onAsynchronousError = options.onAsynchronousError this.protocolManager = options.protocolManager - this.cyPromptManager = options.cyPromptManager this.fullyManageTabs = options.fullyManageTabs this.onServiceWorkerClientEvent = options.onServiceWorkerClientEvent } @@ -228,7 +226,6 @@ export class BrowserCriClient { * @param options.onReconnect callback for when the browser cri client reconnects to the browser * @param options.port the port to which to connect * @param options.protocolManager the protocol manager to use with the browser cri client - * @param options.cyPromptManager the cy.prompt() manager to use with the browser cri client * @param options.onServiceWorkerClientEvent callback for when a service worker fetch event is received * @returns a wrapper around the chrome remote interface that is connected to the browser target */ @@ -241,7 +238,6 @@ export class BrowserCriClient { onReconnect, port, protocolManager, - cyPromptManager, onServiceWorkerClientEvent, } = options @@ -266,7 +262,6 @@ export class BrowserCriClient { browserName, onAsynchronousError, protocolManager, - cyPromptManager, fullyManageTabs, onServiceWorkerClientEvent, }) @@ -569,32 +564,14 @@ export class BrowserCriClient { browserClient: this.browserClient, }) - const currentTarget = this.currentlyAttachedTarget - - const createProtocolTarget = async () => { - // Clone the target here so that we separate the protocol client and the main client. - // This allows us to close the protocol client independently of the main client - // which we do when we exit out of studio in open mode. - if (!this.currentlyAttachedProtocolTarget) { - this.currentlyAttachedProtocolTarget = await currentTarget.clone() - } - - await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) + // Clone the target here so that we separate the protocol client and the main client. + // This allows us to close the protocol client independently of the main client + // which we do when we exit out of studio in open mode. + if (!this.currentlyAttachedProtocolTarget) { + this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone() } - const createCyPromptTarget = async () => { - // Clone the cy.prompt() target here so that we separate the cy.prompt() client and the main client. - if (!this.currentlyAttachedCyPromptTarget) { - this.currentlyAttachedCyPromptTarget = await currentTarget.clone() - } - - await this.cyPromptManager?.connectToBrowser(this.currentlyAttachedCyPromptTarget) - } - - await Promise.all([ - createProtocolTarget(), - createCyPromptTarget(), - ]) + await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget) return this.currentlyAttachedTarget }, this.browserName, this.port) diff --git a/packages/server/test/unit/browsers/browser-cri-client_spec.ts b/packages/server/test/unit/browsers/browser-cri-client_spec.ts index b751ec450ad7..5647f1179a13 100644 --- a/packages/server/test/unit/browsers/browser-cri-client_spec.ts +++ b/packages/server/test/unit/browsers/browser-cri-client_spec.ts @@ -60,14 +60,14 @@ describe('lib/browsers/browser-cri-client', function () { 'chrome-remote-interface': criImport, }) - getClient = ({ protocolManager, cyPromptManager, fullyManageTabs } = {}) => { + getClient = ({ protocolManager, fullyManageTabs } = {}) => { criClientCreateStub = criClientCreateStub.withArgs({ target: 'http://web/socket/url', onAsynchronousError: onError, onReconnect: undefined, protocolManager, fullyManageTabs }).resolves({ send, on, close, }) - return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, cyPromptManager, fullyManageTabs, onServiceWorkerClientEvent }) + return browserCriClient.BrowserCriClient.create({ hosts: ['127.0.0.1'], port: PORT, browserName: 'Chrome', onAsynchronousError: onError, protocolManager, fullyManageTabs, onServiceWorkerClientEvent }) } }) @@ -455,9 +455,8 @@ describe('lib/browsers/browser-cri-client', function () { context('#attachToTargetUrl', function () { it('creates a page client when the passed in url is found', async function () { const mockProtocolClient = {} - const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient), } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) @@ -469,50 +468,39 @@ describe('lib/browsers/browser-cri-client', function () { expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) - expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) }) it('creates a page client when the passed in url is found and notifies the protocol manager and fully managed tabs', async function () { const mockProtocolClient = {} - const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient), } const protocolManager: any = { connectToBrowser: sinon.stub().resolves(), } - const cyPromptManager: any = { - connectToBrowser: sinon.stub().resolves(), - } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) send.withArgs('Target.setDiscoverTargets', { discover: true }) on.withArgs('Target.targetDestroyed', sinon.match.func) criClientCreateStub.withArgs({ target: '1', onAsynchronousError: onError, host: HOST, port: PORT, protocolManager, fullyManageTabs: true, browserClient: { on, send, close } }).resolves(mockPageClient) - const browserClient = await getClient({ protocolManager, cyPromptManager, fullyManageTabs: true }) + const browserClient = await getClient({ protocolManager, fullyManageTabs: true }) const client = await browserClient.attachToTargetUrl('http://foo.com') expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) - expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) expect(protocolManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedProtocolTarget) - expect(cyPromptManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedCyPromptTarget) }) it('creates a page client when the passed in url is found and notifies the protocol manager and fully managed tabs and attaching to target throws', async function () { const mockProtocolClient = {} - const mockCyPromptClient = {} const mockPageClient = { - clone: sinon.stub().onFirstCall().returns(mockProtocolClient).onSecondCall().returns(mockCyPromptClient), + clone: sinon.stub().onFirstCall().returns(mockProtocolClient), } const protocolManager: any = { connectToBrowser: sinon.stub().resolves(), } - const cyPromptManager: any = { - connectToBrowser: sinon.stub().resolves(), - } send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] }) send.withArgs('Target.setDiscoverTargets', { discover: true }) @@ -522,15 +510,13 @@ describe('lib/browsers/browser-cri-client', function () { criClientCreateStub.withArgs({ target: '1', onAsynchronousError: onError, host: HOST, port: PORT, protocolManager, fullyManageTabs: true, browserClient: { on, send, close } }).resolves(mockPageClient) - const browserClient = await getClient({ protocolManager, cyPromptManager, fullyManageTabs: true }) + const browserClient = await getClient({ protocolManager, fullyManageTabs: true }) const client = await browserClient.attachToTargetUrl('http://foo.com') expect(client).to.be.equal(mockPageClient) expect(browserClient.currentlyAttachedProtocolTarget).to.be.equal(mockProtocolClient) - expect(browserClient.currentlyAttachedCyPromptTarget).to.be.equal(mockCyPromptClient) expect(protocolManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedProtocolTarget) - expect(cyPromptManager.connectToBrowser).to.be.calledWith(browserClient.currentlyAttachedCyPromptTarget) // This would throw if the error was not caught await on.withArgs('Target.attachedToTarget').args[0][1]({ targetInfo: { type: 'worker' } })