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 all commits
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
7 changes: 7 additions & 0 deletions packages/driver/cypress/e2e/commands/prompt.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
describe('src/cy/commands/prompt', () => {
it('executes the prompt command', () => {
// 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
}

cy.visit('/fixtures/dom.html')

// TODO: add more tests when cy.prompt is built out, but for now this just
Expand Down
6 changes: 6 additions & 0 deletions packages/driver/src/cy/commands/prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ export default (Commands, Cypress, cy) => {
if (Cypress.config('experimentalPromptCommand')) {
Commands.addAll({
async prompt (message: string) {
if (Cypress.browser.family !== 'chromium' && Cypress.browser.name !== 'electron') {
// 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.')
}

try {
let cloud = initializedCyPrompt

Expand Down
50 changes: 38 additions & 12 deletions 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 Down Expand Up @@ -38,6 +38,7 @@ type BrowserCriClientCreateOptions = {
onReconnect?: (client: CriClient) => void
port: number
protocolManager?: ProtocolManagerShape
cyPromptManager?: CyPromptManagerShape
onServiceWorkerClientEvent: ServiceWorkerEventHandler
}

Expand Down Expand Up @@ -184,10 +185,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 Down Expand Up @@ -466,8 +469,9 @@ 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.currentlyAttachedTarget.close().catch(() => { })
browserCriClient.currentlyAttachedProtocolTarget?.close().catch(() => { })
browserCriClient.currentlyAttachedCyPromptTarget?.close().catch(() => { })

new Bluebird((resolve) => {
// this event could fire either expectedly or unexpectedly
Expand Down Expand Up @@ -565,9 +569,10 @@ export class BrowserCriClient {
// 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)
}

await this.protocolManager?.connectToBrowser(this.currentlyAttachedProtocolTarget)

return this.currentlyAttachedTarget
}, this.browserName, this.port)
}
Expand Down Expand Up @@ -606,8 +611,11 @@ export class BrowserCriClient {

debug('target closed', this.currentlyAttachedTarget.targetId)

await this.currentlyAttachedTarget.close().catch(() => {})
await this.currentlyAttachedProtocolTarget?.close().catch(() => {})
await Promise.all([
this.currentlyAttachedTarget.close().catch(() => {}),
this.currentlyAttachedProtocolTarget?.close().catch(() => {}),
this.currentlyAttachedCyPromptTarget?.close().catch(() => {}),
])

debug('target client closed', this.currentlyAttachedTarget.targetId)
}
Expand All @@ -620,6 +628,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,
Expand All @@ -631,13 +643,24 @@ 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.
this.currentlyAttachedProtocolTarget = await this.currentlyAttachedTarget.clone()
const currentTarget = this.currentlyAttachedTarget

const createProtocolTarget = async () => {
this.currentlyAttachedProtocolTarget = await currentTarget.clone()
}

const createCyPromptTarget = async () => {
this.currentlyAttachedCyPromptTarget = await currentTarget.clone()
}

await Promise.all([
createProtocolTarget(),
createCyPromptTarget(),
])
} else {
this.currentlyAttachedTarget = undefined
this.currentlyAttachedProtocolTarget = undefined
this.currentlyAttachedCyPromptTarget = undefined
}

this.resettingBrowserTargets = false
Expand Down Expand Up @@ -696,8 +719,11 @@ export class BrowserCriClient {
this.connected = false

if (this.currentlyAttachedTarget) {
await this.currentlyAttachedTarget.close()
await this.currentlyAttachedProtocolTarget?.close()
await Promise.all([
this.currentlyAttachedTarget.close(),
this.currentlyAttachedProtocolTarget?.close(),
this.currentlyAttachedCyPromptTarget?.close(),
])
}

await this.browserClient.close()
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
15 changes: 14 additions & 1 deletion 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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class CyPromptLifecycleManager {
cloudDataSource,
ctx,
}: {
projectId: string
projectId?: string
cloudDataSource: CloudDataSource
ctx: DataContext
}): void {
Expand Down Expand Up @@ -88,7 +88,7 @@ export class CyPromptLifecycleManager {
projectId,
cloudDataSource,
}: {
projectId: string
projectId?: string
cloudDataSource: CloudDataSource
}): Promise<CyPromptManager> {
const cyPromptSession = await postCyPromptSession({
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import path from 'path'
interface EnsureCyPromptBundleOptions {
cyPromptPath: string
cyPromptUrl: string
projectId: string
projectId?: string
bundlePath: string
}

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
8 changes: 6 additions & 2 deletions 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 @@ -158,7 +158,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({
Expand Down Expand Up @@ -514,6 +514,10 @@ export class ProjectBase extends EE {
}
},

onCyPromptReady: async (cyPromptManager: CyPromptManagerShape) => {
await 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
5 changes: 4 additions & 1 deletion 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 @@ -545,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',
}
Expand Down
Loading