Skip to content

Commit

Permalink
chore: support experimentalSingleTabRunMode in protocol (#27659)
Browse files Browse the repository at this point in the history
  • Loading branch information
mschile authored Aug 25, 2023
1 parent c763d85 commit 5fe9473
Show file tree
Hide file tree
Showing 33 changed files with 7,215 additions and 344 deletions.
4 changes: 2 additions & 2 deletions packages/server/lib/browsers/cdp_automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ export class CdpAutomation implements CDPClient {
return false
}

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

client.on('Page.frameAttached', this._updateFrameTree(client, 'Page.frameAttached'))
Expand Down
10 changes: 9 additions & 1 deletion packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ export = {
browserCriClient = undefined
},

async connectProtocolToBrowser (options: { protocolManager?: ProtocolManagerShape }) {
const browserCriClient = this._getBrowserCriClient()

if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectProtocolToBrowser')

await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedTarget)
},

async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
debug('connecting to new chrome tab in existing instance with url and debugging port', { url: options.url })

Expand All @@ -456,7 +464,7 @@ export = {

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

await options.protocolManager?.connectToBrowser(pageCriClient)
await this.connectProtocolToBrowser({ protocolManager: options.protocolManager })

await this.attachListeners(options.url, pageCriClient, automation, options)
},
Expand Down
30 changes: 26 additions & 4 deletions packages/server/lib/browsers/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,10 +283,15 @@ export = {
this._clearCache(win.webContents),
])

await browserCriClient?.currentlyAttachedTarget?.send('Page.enable')
const browserCriClient = this._getBrowserCriClient()
const pageCriClient = browserCriClient?.currentlyAttachedTarget

if (!pageCriClient) throw new Error('Missing pageCriClient in _launch')

await pageCriClient.send('Page.enable')

await Promise.all([
protocolManager?.connectToBrowser(cdpAutomation),
this.connectProtocolToBrowser({ protocolManager }),
videoApi && recordVideo(cdpAutomation, videoApi),
this._handleDownloads(win, options.downloadsFolder, automation),
])
Expand All @@ -296,14 +301,15 @@ export = {

await win.loadURL(url)

await cdpAutomation._handlePausedRequests(browserCriClient?.currentlyAttachedTarget)
cdpAutomation._listenForFrameTreeChanges(browserCriClient?.currentlyAttachedTarget)
await cdpAutomation._handlePausedRequests(pageCriClient)
cdpAutomation._listenForFrameTreeChanges(pageCriClient)

return win
},

_enableDebugger () {
debug('debugger: enable Console and Network')
const browserCriClient = this._getBrowserCriClient()

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

const browserCriClient = this._getBrowserCriClient()

return browserCriClient?.currentlyAttachedTarget?.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: dir,
Expand Down Expand Up @@ -370,6 +378,8 @@ export = {
// set both because why not
webContents.userAgent = userAgent

const browserCriClient = this._getBrowserCriClient()

// In addition to the session, also set the user-agent optimistically through CDP. @see https://github.com/cypress-io/cypress/issues/23597
browserCriClient?.currentlyAttachedTarget?.send('Network.setUserAgentOverride', {
userAgent,
Expand All @@ -388,6 +398,10 @@ export = {
})
},

_getBrowserCriClient () {
return browserCriClient
},

/**
* 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.
*/
Expand All @@ -405,6 +419,14 @@ export = {
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron')
},

async connectProtocolToBrowser (options: { protocolManager?: ProtocolManagerShape }) {
const browserCriClient = this._getBrowserCriClient()

if (!browserCriClient?.currentlyAttachedTarget) throw new Error('Missing pageCriClient in connectProtocolToBrowser')

await options.protocolManager?.connectToBrowser(browserCriClient.currentlyAttachedTarget)
},

validateLaunchOptions (launchOptions: typeof utils.defaultLaunchOptions) {
const options: string[] = []

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 @@ -390,6 +390,10 @@ export function connectToExisting () {
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')))
}

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

async function recordVideo (videoApi: RunModeVideoApi) {
const { writeVideoFrame } = await videoApi.useFfmpegVideoController({ webmInput: true })

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 } from '@packages/types'
import { BROWSER_FAMILY, BrowserLaunchOpts, BrowserNewTabOpts, FoundBrowser, ProtocolManagerShape } from '@packages/types'
import type { Browser, BrowserInstance, BrowserLauncher } from './types'
import type { Automation } from '../automation'

Expand Down Expand Up @@ -137,6 +137,12 @@ export = {
return this.getBrowserInstance()
},

async connectProtocolToBrowser (options: { browser: Browser, foundBrowsers?: FoundBrowser[], protocolManager?: ProtocolManagerShape }) {
const browserLauncher = await getBrowserLauncher(options.browser, options.foundBrowsers || [])

await browserLauncher.connectProtocolToBrowser(options)
},

async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation): Promise<BrowserInstance | null> {
const browserLauncher = await getBrowserLauncher(browser, options.browsers)

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 } from '@packages/types'
import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts, ProtocolManagerShape } from '@packages/types'
import type { EventEmitter } from 'events'
import type { Automation } from '../automation'

Expand Down Expand Up @@ -39,4 +39,8 @@ export type BrowserLauncher = {
* Used to clear instance state after the browser has been exited.
*/
clearInstanceState: () => void
/**
* Used to connect the protocol to an existing browser.
*/
connectProtocolToBrowser: (options: { protocolManager?: ProtocolManagerShape }) => Promise<void>
}
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export function connectToExisting () {
throw new Error('Cypress-in-Cypress is not supported for WebKit.')
}

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

/**
* 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].
* 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.
Expand Down
8 changes: 7 additions & 1 deletion packages/server/lib/modes/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ function listenForProjectEnd (project, exit): Bluebird<any> {
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 }) {
if (globalThis.CY_TEST_MOCK?.waitForBrowserToConnect) return Promise.resolve()

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

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

// since we aren't going to be opening a new tab,
// we need to tell the protocol manager to reconnect to the existing browser
if (protocolManager) {
await openProject.connectProtocolToBrowser({ browser, foundBrowsers: project.options.browsers, protocolManager })
}

// since we aren't re-launching the browser, we have to navigate to the next spec instead
debug('navigating to next spec %s', createPublicSpec(spec))

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 @@ -229,6 +229,10 @@ export class OpenProject {
return this.closeOpenProjectAndBrowsers()
}

async connectProtocolToBrowser (options) {
await browsers.connectProtocolToBrowser(options)
}

changeUrlToSpec (spec: Cypress.Spec) {
if (!this.projectBase) {
debug('No projectBase, cannot change url')
Expand Down
52 changes: 52 additions & 0 deletions packages/server/test/unit/browsers/chrome_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,10 @@ describe('lib/browsers/chrome', () => {

context('#connectToNewSpec', () => {
it('launches a new tab, connects a cri client to it, starts video, navigates to the spec url, and handles downloads', async function () {
const protocolManager = {
connectToBrowser: sinon.stub().resolves(),
}

const pageCriClient = {
send: sinon.stub().resolves(),
on: sinon.stub(),
Expand Down Expand Up @@ -521,6 +525,7 @@ describe('lib/browsers/chrome', () => {
onInitializeNewBrowserTab: () => {
onInitializeNewBrowserTabCalled = true
},
protocolManager,
}

sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
Expand All @@ -536,6 +541,53 @@ describe('lib/browsers/chrome', () => {
expect(chrome._navigateUsingCRI).to.be.called
expect(chrome._handleDownloads).to.be.called
expect(onInitializeNewBrowserTabCalled).to.be.true
expect(protocolManager.connectToBrowser).to.be.calledWith(pageCriClient)
})
})

context('#connectProtocolToBrowser', () => {
it('connects to the browser cri client', async function () {
const protocolManager = {
connectToBrowser: sinon.stub().resolves(),
}

const pageCriClient = sinon.stub()

const browserCriClient = {
currentlyAttachedTarget: pageCriClient,
}

sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)

await chrome.connectProtocolToBrowser({ protocolManager })

expect(protocolManager.connectToBrowser).to.be.calledWith(pageCriClient)
})

it('throws error if there is no browser cri client', function () {
const protocolManager = {
connectToBrowser: sinon.stub().resolves(),
}

sinon.stub(chrome, '_getBrowserCriClient').returns(null)

expect(chrome.connectProtocolToBrowser({ protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
expect(protocolManager.connectToBrowser).not.to.be.called
})

it('throws error if there is no page cri client', function () {
const protocolManager = {
connectToBrowser: sinon.stub().resolves(),
}

const browserCriClient = {
currentlyAttachedTarget: null,
}

sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)

expect(chrome.connectProtocolToBrowser({ protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
expect(protocolManager.connectToBrowser).not.to.be.called
})
})

Expand Down
35 changes: 35 additions & 0 deletions packages/server/test/unit/browsers/electron_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const ELECTRON_PID = 10001

describe('lib/browsers/electron', () => {
beforeEach(function () {
this.protocolManager = {
connectToBrowser: sinon.stub().resolves(),
}

this.url = 'https://foo.com'
this.state = {}
this.options = {
Expand Down Expand Up @@ -203,6 +207,31 @@ describe('lib/browsers/electron', () => {
})
})

context('.connectProtocolToBrowser', () => {
it('connects to the browser cri client', async function () {
sinon.stub(electron, '_getBrowserCriClient').returns(this.browserCriClient)

await electron.connectProtocolToBrowser({ protocolManager: this.protocolManager })
expect(this.protocolManager.connectToBrowser).to.be.calledWith(this.pageCriClient)
})

it('throws error if there is no browser cri client', function () {
sinon.stub(electron, '_getBrowserCriClient').returns(null)

expect(electron.connectProtocolToBrowser({ protocolManager: this.protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
expect(this.protocolManager.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.connectProtocolToBrowser({ protocolManager: this.protocolManager })).to.be.rejectedWith('Missing pageCriClient in connectProtocolToBrowser')
expect(this.protocolManager.connectToBrowser).not.to.be.called
})
})

context('._launch', () => {
beforeEach(() => {
sinon.stub(menu, 'set')
Expand Down Expand Up @@ -457,6 +486,12 @@ describe('lib/browsers/electron', () => {

expect(this.pageCriClient.send).to.be.calledWith('Page.getFrameTree')
})

it('connects the protocol manager to the browser', async function () {
await electron._launch(this.win, this.url, this.automation, this.options, undefined, this.protocolManager)

expect(this.protocolManager.connectToBrowser).to.be.calledWith(this.pageCriClient)
})
})
})

Expand Down
6 changes: 6 additions & 0 deletions packages/server/test/unit/browsers/firefox_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,12 @@ describe('lib/browsers/firefox', () => {
})
})

context('#connectProtocolToBrowser', () => {
it('throws error', () => {
expect(firefox.connectProtocolToBrowser).to.throw('Protocol is not yet supported in firefox.')
})
})

context('firefox-util', () => {
context('#setupMarionette', () => {
// @see https://github.com/cypress-io/cypress/issues/7159
Expand Down
13 changes: 13 additions & 0 deletions packages/server/test/unit/browsers/webkit_spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require('../../spec_helper')

import { expect } from 'chai'

import * as webkit from '../../../lib/browsers/webkit'

describe('lib/browsers/webkit', () => {
context('#connectProtocolToBrowser', () => {
it('throws error', () => {
expect(webkit.connectProtocolToBrowser).to.throw('Protocol is not yet supported in WebKit.')
})
})
})
11 changes: 11 additions & 0 deletions packages/server/test/unit/open_project_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,15 @@ describe('lib/open_project', () => {
expect(ProjectBase.prototype.sendFocusBrowserMessage).not.to.have.been.called
})
})

context('#connectProtocolToBrowser', () => {
it('connects protocol to browser', async () => {
sinon.stub(browsers, 'connectProtocolToBrowser').resolves()
const options = sinon.stub()

await openProject.connectProtocolToBrowser(options)

expect(browsers.connectProtocolToBrowser).to.be.calledWith(options)
})
})
})
Loading

5 comments on commit 5fe9473

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5fe9473 Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.0.0/linux-x64/release/13.0.0-5fe94731dc1325bdb7302e272a74360bc216a75e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5fe9473 Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.0.0/darwin-arm64/release/13.0.0-5fe94731dc1325bdb7302e272a74360bc216a75e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5fe9473 Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.0.0/linux-arm64/release/13.0.0-5fe94731dc1325bdb7302e272a74360bc216a75e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5fe9473 Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.0.0/darwin-x64/release/13.0.0-5fe94731dc1325bdb7302e272a74360bc216a75e/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 5fe9473 Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.0.0/win32-x64/release/13.0.0-5fe94731dc1325bdb7302e272a74360bc216a75e/cypress.tgz

Please sign in to comment.