Skip to content

Commit 5fe9473

Browse files
authored
chore: support experimentalSingleTabRunMode in protocol (#27659)
1 parent c763d85 commit 5fe9473

33 files changed

+7215
-344
lines changed

packages/server/lib/browsers/cdp_automation.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ export class CdpAutomation implements CDPClient {
362362
return false
363363
}
364364

365-
_handlePausedRequests = async (client) => {
365+
_handlePausedRequests = async (client: CriClient) => {
366366
// NOTE: only supported in chromium based browsers
367367
await client.send('Fetch.enable', {
368368
// only enable request pausing for documents to determine the AUT iframe
@@ -390,7 +390,7 @@ export class CdpAutomation implements CDPClient {
390390
// we can't get the frame tree during the Fetch.requestPaused event, because
391391
// the CDP is tied up during that event and can't be utilized. so we maintain
392392
// a reference to it that's updated when it's likely to have been changed
393-
_listenForFrameTreeChanges = (client) => {
393+
_listenForFrameTreeChanges = (client: CriClient) => {
394394
debugVerbose('listen for frame tree changes')
395395

396396
client.on('Page.frameAttached', this._updateFrameTree(client, 'Page.frameAttached'))

packages/server/lib/browsers/chrome.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,14 @@ export = {
443443
browserCriClient = undefined
444444
},
445445

446+
async connectProtocolToBrowser (options: { protocolManager?: ProtocolManagerShape }) {
447+
const browserCriClient = this._getBrowserCriClient()
448+
449+
if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectProtocolToBrowser')
450+
451+
await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedTarget)
452+
},
453+
446454
async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
447455
debug('connecting to new chrome tab in existing instance with url and debugging port', { url: options.url })
448456

@@ -456,7 +464,7 @@ export = {
456464

457465
if (!options.url) throw new Error('Missing url in connectToNewSpec')
458466

459-
await options.protocolManager?.connectToBrowser(pageCriClient)
467+
await this.connectProtocolToBrowser({ protocolManager: options.protocolManager })
460468

461469
await this.attachListeners(options.url, pageCriClient, automation, options)
462470
},

packages/server/lib/browsers/electron.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,15 @@ export = {
283283
this._clearCache(win.webContents),
284284
])
285285

286-
await browserCriClient?.currentlyAttachedTarget?.send('Page.enable')
286+
const browserCriClient = this._getBrowserCriClient()
287+
const pageCriClient = browserCriClient?.currentlyAttachedTarget
288+
289+
if (!pageCriClient) throw new Error('Missing pageCriClient in _launch')
290+
291+
await pageCriClient.send('Page.enable')
287292

288293
await Promise.all([
289-
protocolManager?.connectToBrowser(cdpAutomation),
294+
this.connectProtocolToBrowser({ protocolManager }),
290295
videoApi && recordVideo(cdpAutomation, videoApi),
291296
this._handleDownloads(win, options.downloadsFolder, automation),
292297
])
@@ -296,14 +301,15 @@ export = {
296301

297302
await win.loadURL(url)
298303

299-
await cdpAutomation._handlePausedRequests(browserCriClient?.currentlyAttachedTarget)
300-
cdpAutomation._listenForFrameTreeChanges(browserCriClient?.currentlyAttachedTarget)
304+
await cdpAutomation._handlePausedRequests(pageCriClient)
305+
cdpAutomation._listenForFrameTreeChanges(pageCriClient)
301306

302307
return win
303308
},
304309

305310
_enableDebugger () {
306311
debug('debugger: enable Console and Network')
312+
const browserCriClient = this._getBrowserCriClient()
307313

308314
return browserCriClient?.currentlyAttachedTarget?.send('Console.enable')
309315
},
@@ -333,6 +339,8 @@ export = {
333339
// avoid adding redundant `will-download` handlers if session is reused for next spec
334340
win.on('closed', () => session.removeListener('will-download', onWillDownload))
335341

342+
const browserCriClient = this._getBrowserCriClient()
343+
336344
return browserCriClient?.currentlyAttachedTarget?.send('Page.setDownloadBehavior', {
337345
behavior: 'allow',
338346
downloadPath: dir,
@@ -370,6 +378,8 @@ export = {
370378
// set both because why not
371379
webContents.userAgent = userAgent
372380

381+
const browserCriClient = this._getBrowserCriClient()
382+
373383
// In addition to the session, also set the user-agent optimistically through CDP. @see https://github.com/cypress-io/cypress/issues/23597
374384
browserCriClient?.currentlyAttachedTarget?.send('Network.setUserAgentOverride', {
375385
userAgent,
@@ -388,6 +398,10 @@ export = {
388398
})
389399
},
390400

401+
_getBrowserCriClient () {
402+
return browserCriClient
403+
},
404+
391405
/**
392406
* Clear instance state for the electron instance, this is normally called on kill or on exit, for electron there isn't any state to clear.
393407
*/
@@ -405,6 +419,14 @@ export = {
405419
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron')
406420
},
407421

422+
async connectProtocolToBrowser (options: { protocolManager?: ProtocolManagerShape }) {
423+
const browserCriClient = this._getBrowserCriClient()
424+
425+
if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectProtocolToBrowser')
426+
427+
await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedTarget)
428+
},
429+
408430
validateLaunchOptions (launchOptions: typeof utils.defaultLaunchOptions) {
409431
const options: string[] = []
410432

packages/server/lib/browsers/firefox.ts

+4
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ export function connectToExisting () {
390390
getCtx().onWarning(getError('UNEXPECTED_INTERNAL_ERROR', new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for firefox')))
391391
}
392392

393+
export function connectProtocolToBrowser (): Promise<void> {
394+
throw new Error('Protocol is not yet supported in firefox.')
395+
}
396+
393397
async function recordVideo (videoApi: RunModeVideoApi) {
394398
const { writeVideoFrame } = await videoApi.useFfmpegVideoController({ webmInput: true })
395399

packages/server/lib/browsers/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import check from 'check-more-types'
77
import { exec } from 'child_process'
88
import util from 'util'
99
import os from 'os'
10-
import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser } from '@packages/types'
10+
import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape } from '@packages/types'
1111
import type { Browser, BrowserInstance, BrowserLauncher } from './types'
1212
import type { Automation } from '../automation'
1313

@@ -137,6 +137,12 @@ export = {
137137
return this.getBrowserInstance()
138138
},
139139

140+
async connectProtocolToBrowser (options: { browser: Browser, foundBrowsers?: FoundBrowser[], protocolManager?: ProtocolManagerShape }) {
141+
const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || [])
142+
143+
await browserLauncher.connectProtocolToBrowser(options)
144+
},
145+
140146
async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation): Promise<BrowserInstance | null> {
141147
const browserLauncher = await getBrowserLauncher(browser, options.browsers)
142148

packages/server/lib/browsers/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types'
1+
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape } from '@packages/types'
22
import type { EventEmitter } from 'events'
33
import type { Automation } from '../automation'
44

@@ -39,4 +39,8 @@ export type BrowserLauncher = {
3939
* Used to clear instance state after the browser has been exited.
4040
*/
4141
clearInstanceState: () => void
42+
/**
43+
* Used to connect the protocol to an existing browser.
44+
*/
45+
connectProtocolToBrowser: (options: { protocolManager?: ProtocolManagerShape }) => Promise<void>
4246
}

packages/server/lib/browsers/webkit.ts

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export function connectToExisting () {
3636
throw new Error('Cypress-in-Cypress is not supported for WebKit.')
3737
}
3838

39+
export function connectProtocolToBrowser (): Promise<void> {
40+
throw new Error('Protocol is not yet supported in WebKit.')
41+
}
42+
3943
/**
4044
* Playwright adds an `exit` event listener to run a cleanup process. It tries to use the current binary to run a Node script by passing it as argv[1].
4145
* However, the Electron binary does not support an entrypoint, leading Cypress to think it's being opened in global mode (no args) when this fn is called.

packages/server/lib/modes/run.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ function listenForProjectEnd (project, exit): Bluebird<any> {
464464
async function waitForBrowserToConnect (options: { project: Project, socketId: string, onError: (err: Error) => void, spec: SpecWithRelativeRoot, isFirstSpecInBrowser: boolean, testingType: string, experimentalSingleTabRunMode: boolean, browser: Browser, screenshots: ScreenshotMetadata[], projectRoot: string, shouldLaunchNewTab: boolean, webSecurity: boolean, videoRecording?: VideoRecording, protocolManager?: ProtocolManager }) {
465465
if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve()
466466

467-
const { project, socketId, onError, spec } = options
467+
const { project, socketId, onError, spec, browser, protocolManager } = options
468468
const browserTimeout = Number(process.env.CYPRESS_INTERNAL_BROWSER_CONNECT_TIMEOUT || 60000)
469469
let browserLaunchAttempt = 1
470470

@@ -492,6 +492,12 @@ async function waitForBrowserToConnect (options: { project: Project, socketId: s
492492
openProject.updateTelemetryContext(JSON.stringify(telemetry.getActiveContextObject()))
493493
}
494494

495+
// since we aren't going to be opening a new tab,
496+
// we need to tell the protocol manager to reconnect to the existing browser
497+
if (protocolManager) {
498+
await openProject.connectProtocolToBrowser({ browser, foundBrowsers: project.options.browsers, protocolManager })
499+
}
500+
495501
// since we aren't re-launching the browser, we have to navigate to the next spec instead
496502
debug('navigating to next spec %s', createPublicSpec(spec))
497503

packages/server/lib/open_project.ts

+4
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ export class OpenProject {
229229
return this.closeOpenProjectAndBrowsers()
230230
}
231231

232+
async connectProtocolToBrowser (options) {
233+
await browsers.connectProtocolToBrowser(options)
234+
}
235+
232236
changeUrlToSpec (spec: Cypress.Spec) {
233237
if (!this.projectBase) {
234238
debug('No projectBase, cannot change url')

packages/server/test/unit/browsers/chrome_spec.js

+52
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,10 @@ describe('lib/browsers/chrome', () => {
491491

492492
context('#connectToNewSpec', () => {
493493
it('launches a new tab, connects a cri client to it, starts video, navigates to the spec url, and handles downloads', async function () {
494+
const protocolManager = {
495+
connectToBrowser: sinon.stub().resolves(),
496+
}
497+
494498
const pageCriClient = {
495499
send: sinon.stub().resolves(),
496500
on: sinon.stub(),
@@ -521,6 +525,7 @@ describe('lib/browsers/chrome', () => {
521525
onInitializeNewBrowserTab: () => {
522526
onInitializeNewBrowserTabCalled = true
523527
},
528+
protocolManager,
524529
}
525530

526531
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
@@ -536,6 +541,53 @@ describe('lib/browsers/chrome', () => {
536541
expect(chrome._navigateUsingCRI).to.be.called
537542
expect(chrome._handleDownloads).to.be.called
538543
expect(onInitializeNewBrowserTabCalled).to.be.true
544+
expect(protocolManager.connectToBrowser).to.be.calledWith(pageCriClient)
545+
})
546+
})
547+
548+
context('#connectProtocolToBrowser', () => {
549+
it('connects to the browser cri client', async function () {
550+
const protocolManager = {
551+
connectToBrowser: sinon.stub().resolves(),
552+
}
553+
554+
const pageCriClient = sinon.stub()
555+
556+
const browserCriClient = {
557+
currentlyAttachedTarget: pageCriClient,
558+
}
559+
560+
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
561+
562+
await chrome.connectProtocolToBrowser({ protocolManager })
563+
564+
expect(protocolManager.connectToBrowser).to.be.calledWith(pageCriClient)
565+
})
566+
567+
it('throws error if there is no browser cri client', function () {
568+
const protocolManager = {
569+
connectToBrowser: sinon.stub().resolves(),
570+
}
571+
572+
sinon.stub(chrome, '_getBrowserCriClient').returns(null)
573+
574+
expect(chrome.connectProtocolToBrowser({ protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
575+
expect(protocolManager.connectToBrowser).not.to.be.called
576+
})
577+
578+
it('throws error if there is no page cri client', function () {
579+
const protocolManager = {
580+
connectToBrowser: sinon.stub().resolves(),
581+
}
582+
583+
const browserCriClient = {
584+
currentlyAttachedTarget: null,
585+
}
586+
587+
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
588+
589+
expect(chrome.connectProtocolToBrowser({ protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
590+
expect(protocolManager.connectToBrowser).not.to.be.called
539591
})
540592
})
541593

packages/server/test/unit/browsers/electron_spec.js

+35
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const ELECTRON_PID = 10001
1818

1919
describe('lib/browsers/electron', () => {
2020
beforeEach(function () {
21+
this.protocolManager = {
22+
connectToBrowser: sinon.stub().resolves(),
23+
}
24+
2125
this.url = 'https://foo.com'
2226
this.state = {}
2327
this.options = {
@@ -203,6 +207,31 @@ describe('lib/browsers/electron', () => {
203207
})
204208
})
205209

210+
context('.connectProtocolToBrowser', () => {
211+
it('connects to the browser cri client', async function () {
212+
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)
213+
214+
await electron.connectProtocolToBrowser({ protocolManager: this.protocolManager })
215+
expect(this.protocolManager.connectToBrowser).to.be.calledWith(this.pageCriClient)
216+
})
217+
218+
it('throws error if there is no browser cri client', function () {
219+
sinon.stub(electron, '_getBrowserCriClient').returns(null)
220+
221+
expect(electron.connectProtocolToBrowser({ protocolManager: this.protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
222+
expect(this.protocolManager.connectToBrowser).not.to.be.called
223+
})
224+
225+
it('throws error if there is no page cri client', async function () {
226+
this.browserCriClient.currentlyAttachedTarget = null
227+
228+
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)
229+
230+
expect(electron.connectProtocolToBrowser({ protocolManager: this.protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
231+
expect(this.protocolManager.connectToBrowser).not.to.be.called
232+
})
233+
})
234+
206235
context('._launch', () => {
207236
beforeEach(() => {
208237
sinon.stub(menu, 'set')
@@ -457,6 +486,12 @@ describe('lib/browsers/electron', () => {
457486

458487
expect(this.pageCriClient.send).to.be.calledWith('Page.getFrameTree')
459488
})
489+
490+
it('connects the protocol manager to the browser', async function () {
491+
await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager)
492+
493+
expect(this.protocolManager.connectToBrowser).to.be.calledWith(this.pageCriClient)
494+
})
460495
})
461496
})
462497

packages/server/test/unit/browsers/firefox_spec.ts

+6
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ describe('lib/browsers/firefox', () => {
434434
})
435435
})
436436

437+
context('#connectProtocolToBrowser', () => {
438+
it('throws error', () => {
439+
expect(firefox.connectProtocolToBrowser).to.throw('Protocol is not yet supported in firefox.')
440+
})
441+
})
442+
437443
context('firefox-util', () => {
438444
context('#setupMarionette', () => {
439445
// @see https://github.com/cypress-io/cypress/issues/7159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require('../../spec_helper')
2+
3+
import { expect } from 'chai'
4+
5+
import * as webkit from '../../../lib/browsers/webkit'
6+
7+
describe('lib/browsers/webkit', () => {
8+
context('#connectProtocolToBrowser', () => {
9+
it('throws error', () => {
10+
expect(webkit.connectProtocolToBrowser).to.throw('Protocol is not yet supported in WebKit.')
11+
})
12+
})
13+
})

packages/server/test/unit/open_project_spec.js

+11
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,15 @@ describe('lib/open_project', () => {
255255
expect(ProjectBase.prototype.sendFocusBrowserMessage).not.to.have.been.called
256256
})
257257
})
258+
259+
context('#connectProtocolToBrowser', () => {
260+
it('connects protocol to browser', async () => {
261+
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
262+
const options = sinon.stub()
263+
264+
await openProject.connectProtocolToBrowser(options)
265+
266+
expect(browsers.connectProtocolToBrowser).to.be.calledWith(options)
267+
})
268+
})
258269
})

0 commit comments

Comments
 (0)