Skip to content

chore: add cdp connection to cy prompt #31806

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
61d17f6
chore: add cdp connection to cy prompt
ryanthemanuel May 30, 2025
0f1dcf9
minor fix
ryanthemanuel May 30, 2025
e298034
fix type build
ryanthemanuel May 30, 2025
2e924d5
try to fix build
ryanthemanuel May 30, 2025
1b2643d
Update packages/server/lib/browsers/browser-cri-client.ts
ryanthemanuel May 30, 2025
8c91b7b
Update packages/server/lib/browsers/browser-cri-client.ts
ryanthemanuel May 30, 2025
497e916
do not support prompt in firefox or webkit
ryanthemanuel Jun 1, 2025
1a9ea8a
rework timing of lifecycle
ryanthemanuel Jun 1, 2025
60dd4cc
Merge branch 'feat/cy-prompt' into ryanm/chore/add-cdp-connection-to-…
ryanthemanuel Jun 2, 2025
833e267
refactor
ryanthemanuel Jun 2, 2025
24ae068
fix tests
ryanthemanuel Jun 2, 2025
e341257
troubleshooting
ryanthemanuel Jun 2, 2025
54d2953
troubleshooting
ryanthemanuel Jun 2, 2025
ffed286
fix tests
ryanthemanuel Jun 2, 2025
f15d4a0
Merge branch 'feat/cy-prompt' into ryanm/chore/add-cdp-connection-to-…
ryanthemanuel Jun 2, 2025
a7f0c8c
additional troubleshooting
ryanthemanuel Jun 2, 2025
3bd37d0
additional troubleshooting
ryanthemanuel Jun 2, 2025
66a5dcf
additional troubleshooting
ryanthemanuel Jun 2, 2025
2445b7c
attempt to fix build
ryanthemanuel Jun 3, 2025
b7cd38e
add back
ryanthemanuel Jun 3, 2025
deef67a
debugging
ryanthemanuel Jun 3, 2025
c006eab
debugging
ryanthemanuel Jun 3, 2025
ae144de
debugging
ryanthemanuel Jun 3, 2025
5b85e00
debugging
ryanthemanuel Jun 3, 2025
b64b9d9
clean up
ryanthemanuel Jun 3, 2025
3b21906
fix unit tests
ryanthemanuel Jun 3, 2025
3351489
rework
ryanthemanuel Jun 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/server/lib/browsers/browser-cri-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -26,6 +26,7 @@ type BrowserCriClientOptions = {
browserName: string
onAsynchronousError: (err: CypressError) => void
protocolManager?: ProtocolManagerShape
cyPromptManager?: CyPromptManagerShape
fullyManageTabs?: boolean
onServiceWorkerClientEvent: ServiceWorkerEventHandler
}
Expand All @@ -38,6 +39,7 @@ type BrowserCriClientCreateOptions = {
onReconnect?: (client: CriClient) => void
port: number
protocolManager?: ProtocolManagerShape
cyPromptManager?: CyPromptManagerShape
onServiceWorkerClientEvent: ServiceWorkerEventHandler
}

Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
*/
Expand All @@ -235,6 +241,7 @@ export class BrowserCriClient {
onReconnect,
port,
protocolManager,
cyPromptManager,
onServiceWorkerClientEvent,
} = options

Expand All @@ -259,6 +266,7 @@ export class BrowserCriClient {
browserName,
onAsynchronousError,
protocolManager,
cyPromptManager,
fullyManageTabs,
onServiceWorkerClientEvent,
})
Expand Down Expand Up @@ -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)
}
Expand Down
14 changes: 13 additions & 1 deletion packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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()

Expand Down
17 changes: 15 additions & 2 deletions packages/server/lib/browsers/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 })
}
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,10 @@ export function connectProtocolToBrowser (): Promise<void> {
throw new Error('Protocol is not yet supported in firefox.')
}

export function connectCyPromptToBrowser (): Promise<void> {
throw new Error('CyPrompt is not yet supported in firefox.')
}

export function closeProtocolConnection (): Promise<void> {
throw new Error('Protocol is not yet supported in firefox.')
}
Expand Down
8 changes: 7 additions & 1 deletion packages/server/lib/browsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 || [])

Expand Down
6 changes: 5 additions & 1 deletion packages/server/lib/browsers/types.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -45,6 +45,10 @@ export type BrowserLauncher = {
* Used to connect the protocol to an existing browser.
*/
connectProtocolToBrowser: (options: { protocolManager?: ProtocolManagerShape }) => Promise<void>
/**
* Used to connect the cy prompt to an existing browser.
*/
connectCyPromptToBrowser: (options: { cyPromptManager?: CyPromptManagerShape }) => Promise<void>
/**
* Closes the protocol connection to the browser.
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export function connectProtocolToBrowser (): Promise<void> {
throw new Error('Protocol is not yet supported in WebKit.')
}

export function connectCyPromptToBrowser (): Promise<void> {
throw new Error('CyPrompt is not yet supported in WebKit.')
}

export function closeProtocolConnection (): Promise<void> {
throw new Error('Protocol is not yet supported in WebKit.')
}
Expand Down
11 changes: 9 additions & 2 deletions packages/server/lib/cloud/cy-prompt/CyPromptManager.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -40,7 +40,13 @@ export class CyPromptManager implements CyPromptManagerShape {

async handleBackendRequest (eventName: string, ...args: any[]): Promise<any> {
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)
}
}

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/open_project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
6 changes: 5 additions & 1 deletion packages/server/lib/project-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions packages/server/lib/socket-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export class SocketBase {
onCaptureVideoFrames () {},
onStudioInit () {},
onStudioDestroy () {},
onCyPromptReady () {},
})

let automationClient
Expand Down Expand Up @@ -451,6 +452,7 @@ export class SocketBase {
let cyPrompt: CyPromptManagerShape | undefined

getCtx().coreData.cyPromptLifecycleManager?.registerCyPromptReadyListener((cp) => {
options.onCyPromptReady(cp)
cyPrompt = cp
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="cypress" />

import type { CyPromptServerShape, CyPromptServerDefaultShape } from '@packages/types'
import type { CyPromptServerShape, CyPromptServerDefaultShape, CyPromptCDPClient } from '@packages/types'
import type { Router } from 'express'

class CyPromptServer implements CyPromptServerShape {
Expand All @@ -11,6 +11,10 @@ class CyPromptServer implements CyPromptServerShape {
handleBackendRequest (eventName: string, ...args: any[]): Promise<any> {
return Promise.resolve()
}

connectToBrowser (criClient: CyPromptCDPClient): void {
// This is a test implementation that does nothing
}
}

const cyPromptServerDefault: CyPromptServerDefaultShape = {
Expand Down
Loading