Skip to content

Commit

Permalink
chore: add after:browser:launch node event (#28180)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbreiding committed Nov 6, 2023
1 parent eab1730 commit 934f215
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 71 deletions.
9 changes: 7 additions & 2 deletions cli/types/cypress.d.ts
Expand Up @@ -6009,7 +6009,11 @@ declare namespace Cypress {
(fn: (currentSubject: Subject) => void): Chainable<Subject>
}

interface BrowserLaunchOptions {
interface AfterBrowserLaunchDetails {
webSocketDebuggerUrl: string
}

interface BeforeBrowserLaunchOptions {
extensions: string[]
preferences: { [key: string]: any }
args: string[]
Expand Down Expand Up @@ -6090,12 +6094,13 @@ declare namespace Cypress {
}

interface PluginEvents {
(action: 'after:browser:launch', fn: (browser: Browser, browserLaunchDetails: AfterBrowserLaunchDetails) => void | Promise<void>): void
(action: 'after:run', fn: (results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult) => void | Promise<void>): void
(action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise<AfterScreenshotReturnObject>): void
(action: 'after:spec', fn: (spec: Spec, results: CypressCommandLine.RunResult) => void | Promise<void>): void
(action: 'before:run', fn: (runDetails: BeforeRunDetails) => void | Promise<void>): void
(action: 'before:spec', fn: (spec: Spec) => void | Promise<void>): void
(action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise<BrowserLaunchOptions>): void
(action: 'before:browser:launch', fn: (browser: Browser, afterBrowserLaunchOptions: BeforeBrowserLaunchOptions) => void | Promise<void> | BeforeBrowserLaunchOptions | Promise<BeforeBrowserLaunchOptions>): void
(action: 'file:preprocessor', fn: (file: FileObject) => string | Promise<string>): void
(action: 'dev-server:start', fn: (file: DevServerConfig) => Promise<ResolvedDevServerConfig>): void
(action: 'task', tasks: Tasks): void
Expand Down
7 changes: 7 additions & 0 deletions packages/server/lib/browsers/browser-cri-client.ts
Expand Up @@ -574,6 +574,13 @@ export class BrowserCriClient {
this.extraTargetClients.delete(targetId)
}

/**
* @returns the websocket debugger URL for the currently connected browser
*/
getWebSocketDebuggerUrl () {
return this.versionInfo.webSocketDebuggerUrl
}

/**
* Closes the browser client socket as well as the socket for the currently attached page target
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/chrome.ts
Expand Up @@ -645,6 +645,10 @@ export = {

await this.attachListeners(url, pageCriClient, automation, options, browser)

await utils.executeAfterBrowserLaunch(browser, {
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
})

// return the launched browser process
// with additional method to close the remote connection
return launchedBrowser
Expand Down
23 changes: 17 additions & 6 deletions packages/server/lib/browsers/cri-client.ts
Expand Up @@ -265,15 +265,26 @@ export const create = async ({

maybeDebugCdpMessages(cri)

// Only reconnect when we're not running cypress in cypress. There are a lot of disconnects that happen that we don't want to reconnect on
if (!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
// Having a host set indicates that this is the child cri target, a.k.a.
// the main Cypress tab (as opposed to the root browser cri target)
const isChildTarget = !!host

// don't reconnect in these circumstances
if (
// is a child target. we only need to reconnect the root browser target
!isChildTarget
// running cypress in cypress - there are a lot of disconnects that happen
// that we don't want to reconnect on
&& !process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF
) {
cri.on('disconnect', retryReconnect)
}

// We only want to try and add child target traffic if we have a host set. This indicates that this is the child cri client.
// Browser cri traffic is handled in browser-cri-client.ts. The basic approach here is we attach to targets and enable network traffic
// We must attach in a paused state so that we can enable network traffic before the target starts running.
if (host) {
// We're only interested in child target traffic. Browser cri traffic is
// handled in browser-cri-client.ts. The basic approach here is we attach
// to targets and enable network traffic. We must attach in a paused state
// so that we can enable network traffic before the target starts running.
if (isChildTarget) {
cri.on('Target.targetCrashed', async (event) => {
if (event.targetId !== target) {
return
Expand Down
4 changes: 4 additions & 0 deletions packages/server/lib/browsers/electron.ts
Expand Up @@ -539,6 +539,10 @@ export = {
},
}) as BrowserInstance

await utils.executeAfterBrowserLaunch(browser, {
webSocketDebuggerUrl: browserCriClient!.getWebSocketDebuggerUrl(),
})

return instance
},
}
8 changes: 6 additions & 2 deletions packages/server/lib/browsers/firefox.ts
Expand Up @@ -347,7 +347,7 @@ toolbar {
`

let browserCriClient
let browserCriClient: BrowserCriClient | undefined

export function _createDetachedInstance (browserInstance: BrowserInstance, browserCriClient?: BrowserCriClient): BrowserInstance {
const detachedInstance: BrowserInstance = new EventEmitter() as BrowserInstance
Expand Down Expand Up @@ -382,7 +382,7 @@ export function clearInstanceState (options: GracefulShutdownOptions = {}) {
}

export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient)
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient!)
}

export function connectToExisting () {
Expand Down Expand Up @@ -573,6 +573,10 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc

return originalBrowserKill.apply(browserInstance, args)
}

await utils.executeAfterBrowserLaunch(browser, {
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
})
} catch (err) {
errors.throwErr('FIREFOX_COULD_NOT_CONNECT', err)
}
Expand Down
24 changes: 24 additions & 0 deletions packages/server/lib/browsers/utils.ts
Expand Up @@ -7,6 +7,7 @@ import * as plugins from '../plugins'
import { getError } from '@packages/errors'
import * as launcher from '@packages/launcher'
import type { Automation } from '../automation'
import type { Browser } from './types'
import type { CriClient } from './cri-client'

declare global {
Expand Down Expand Up @@ -157,6 +158,27 @@ async function executeBeforeBrowserLaunch (browser, launchOptions: typeof defaul
return launchOptions
}

interface AfterBrowserLaunchDetails {
webSocketDebuggerUrl: string | never
}

async function executeAfterBrowserLaunch (browser: Browser, options: AfterBrowserLaunchDetails) {
if (plugins.has('after:browser:launch')) {
const span = telemetry.startSpan({ name: 'lifecycle:after:browser:launch' })

span?.setAttribute({
name: browser.name,
channel: browser.channel,
version: browser.version,
isHeadless: browser.isHeadless,
})

await plugins.execute('after:browser:launch', browser, options)

span?.end()
}
}

function extendLaunchOptionsFromPlugins (launchOptions, pluginConfigResult, options: BrowserLaunchOpts) {
// if we returned an array from the plugin
// then we know the user is using the deprecated
Expand Down Expand Up @@ -423,6 +445,8 @@ export = {

extendLaunchOptionsFromPlugins,

executeAfterBrowserLaunch,

executeBeforeBrowserLaunch,

defaultLaunchOptions,
Expand Down
7 changes: 6 additions & 1 deletion packages/server/lib/browsers/webkit.ts
Expand Up @@ -101,7 +101,8 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc

removeBadExitListener()

const pwBrowser = await pw.webkit.connect(pwServer.wsEndpoint())
const websocketUrl = pwServer.wsEndpoint()
const pwBrowser = await pw.webkit.connect(websocketUrl)

wkAutomation = await WebKitAutomation.create({
automation,
Expand Down Expand Up @@ -147,5 +148,9 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
}
}

await utils.executeAfterBrowserLaunch(browser, {
webSocketDebuggerUrl: websocketUrl,
})

return new WkInstance()
}
2 changes: 1 addition & 1 deletion packages/server/lib/plugins/child/browser_launch.js
Expand Up @@ -3,7 +3,7 @@ const util = require('../util')
const ARRAY_METHODS = ['concat', 'push', 'unshift', 'slice', 'pop', 'shift', 'slice', 'splice', 'filter', 'map', 'forEach', 'reduce', 'reverse', 'splice', 'includes']

module.exports = {
wrap (ipc, invoke, ids, args) {
wrapBefore (ipc, invoke, ids, args) {
// TODO: remove in next breaking release
// This will send a warning message when a deprecated API is used
// define array-like functions on this object so we can warn about using deprecated array API
Expand Down
4 changes: 3 additions & 1 deletion packages/server/lib/plugins/child/run_plugins.js
Expand Up @@ -169,7 +169,9 @@ class RunPlugins {
case '_get:task:body':
return this.taskGetBody(ids, args)
case 'before:browser:launch':
return browserLaunch.wrap(this.ipc, this.invoke, ids, args)
return browserLaunch.wrapBefore(this.ipc, this.invoke, ids, args)
case 'after:browser:launch':
return util.wrapChildPromise(this.ipc, this.invoke, ids, args)
default:
debug('unexpected execute message:', event, args)

Expand Down
7 changes: 6 additions & 1 deletion packages/server/lib/plugins/child/validate_event.js
Expand Up @@ -27,6 +27,7 @@ const eventValidators = {
'_get:task:body': isFunction,
'_get:task:keys': isFunction,
'_process:cross:origin:callback': isFunction,
'after:browser:launch': isFunction,
'after:run': isFunction,
'after:screenshot': isFunction,
'after:spec': isFunction,
Expand All @@ -42,7 +43,11 @@ const validateEvent = (event, handler, config, errConstructorFn) => {
const validator = eventValidators[event]

if (!validator) {
const userEvents = _.reject(_.keys(eventValidators), (event) => event.startsWith('_'))
const userEvents = _.reject(_.keys(eventValidators), (event) => {
// we're currently not documenting after:browser:launch, so it shouldn't
// appear in the list of valid events
return event.startsWith('_') || event === 'after:browser:launch'
})

const error = new Error(`invalid event name registered: ${event}`)

Expand Down
2 changes: 2 additions & 0 deletions packages/server/test/integration/cypress_spec.js
Expand Up @@ -1006,6 +1006,7 @@ describe('lib/cypress', () => {
ensureMinimumProtocolVersion: sinon.stub().resolves(),
attachToTargetUrl: sinon.stub().resolves(criClient),
close: sinon.stub().resolves(),
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
}

const cdpAutomation = {
Expand Down Expand Up @@ -1076,6 +1077,7 @@ describe('lib/cypress', () => {
attachToTargetUrl: sinon.stub().resolves(criClient),
currentlyAttachedTarget: criClient,
close: sinon.stub().resolves(),
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
}

sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient)
Expand Down
31 changes: 28 additions & 3 deletions packages/server/test/unit/browsers/chrome_spec.js
Expand Up @@ -33,6 +33,7 @@ describe('lib/browsers/chrome', () => {
attachToTargetUrl: sinon.stub().resolves(this.pageCriClient),
close: sinon.stub().resolves(),
ensureMinimumProtocolVersion: sinon.stub().withArgs('1.3').resolves(),
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
}

this.automation = {
Expand Down Expand Up @@ -93,14 +94,14 @@ describe('lib/browsers/chrome', () => {
})
})

it('is noop without before:browser:launch', function () {
it('executeBeforeBrowserLaunch is noop if before:browser:launch is not registered', function () {
return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)
.then(() => {
expect(plugins.execute).not.to.be.called
expect(plugins.execute).not.to.be.calledWith('before:browser:launch')
})
})

it('is noop if newArgs are not returned', function () {
it('uses default args if new args are not returned from before:browser:launch', function () {
const args = []

sinon.stub(chrome, '_getArgs').returns(args)
Expand Down Expand Up @@ -304,6 +305,30 @@ describe('lib/browsers/chrome', () => {
return expect(chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)).to.be.rejectedWith('Cypress requires at least Chrome 64.')
})

it('sends after:browser:launch with debugger url', function () {
const args = []
const browser = { isHeadless: true }

sinon.stub(chrome, '_getArgs').returns(args)
sinon.stub(plugins, 'has').returns(true)

plugins.execute.resolves(null)

return chrome.open(browser, 'http://', openOpts, this.automation)
.then(() => {
expect(plugins.execute).to.be.calledWith('after:browser:launch', browser, {
webSocketDebuggerUrl: 'ws://debugger',
})
})
})

it('executeAfterBrowserLaunch is noop if after:browser:launch is not registered', function () {
return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)
.then(() => {
expect(plugins.execute).not.to.be.calledWith('after:browser:launch')
})
})

describe('downloads', function () {
it('pushes create:download after download begins', function () {
const downloadData = {
Expand Down
34 changes: 30 additions & 4 deletions packages/server/test/unit/browsers/electron_spec.js
Expand Up @@ -80,6 +80,7 @@ describe('lib/browsers/electron', () => {
attachToTargetUrl: sinon.stub().resolves(this.pageCriClient),
currentlyAttachedTarget: this.pageCriClient,
close: sinon.stub().resolves(),
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
}

sinon.stub(BrowserCriClient, 'create').resolves(this.browserCriClient)
Expand Down Expand Up @@ -111,8 +112,11 @@ describe('lib/browsers/electron', () => {
})

context('.open', () => {
beforeEach(function () {
return this.stubForOpen()
beforeEach(async function () {
// shortcut to set the browserCriClient singleton variable
await electron._getAutomation({}, { onError: () => {} }, {})

await this.stubForOpen()
})

it('calls render with url, state, and options', function () {
Expand Down Expand Up @@ -152,7 +156,7 @@ describe('lib/browsers/electron', () => {
})
})

it('is noop when before:browser:launch yields null', function () {
it('executeBeforeBrowserLaunch is noop when before:browser:launch yields null', function () {
plugins.has.returns(true)
plugins.execute.resolves(null)

Expand Down Expand Up @@ -207,6 +211,25 @@ describe('lib/browsers/electron', () => {
expect(Windows.removeAllExtensions).to.be.calledTwice
})
})

it('sends after:browser:launch with debugger url', function () {
plugins.has.returns(true)
plugins.execute.resolves(null)

return electron.open('electron', this.url, this.options, this.automation)
.then(() => {
expect(plugins.execute).to.be.calledWith('after:browser:launch', 'electron', {
webSocketDebuggerUrl: 'ws://debugger',
})
})
})

it('executeAfterBrowserLaunch is noop if after:browser:launch is not registered', function () {
return electron.open('electron', this.url, this.options, this.automation)
.then(() => {
expect(plugins.execute).not.to.be.calledWith('after:browser:launch')
})
})
})

context('.connectProtocolToBrowser', () => {
Expand Down Expand Up @@ -821,7 +844,10 @@ describe('lib/browsers/electron', () => {
expect(electron._launchChild).to.be.calledWith(this.url, parentWindow, this.options.projectRoot, this.state, this.options, this.automation)
})

it('adds pid of new BrowserWindow to allPids list', function () {
it('adds pid of new BrowserWindow to allPids list', async function () {
// shortcut to set the browserCriClient singleton variable
await electron._getAutomation({}, { onError: () => {} }, {})

const opts = electron._defaultOptions(this.options.projectRoot, this.state, this.options)

const NEW_WINDOW_PID = ELECTRON_PID * 2
Expand Down

5 comments on commit 934f215

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 934f215 Nov 6, 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.4.1/linux-x64/develop-934f2157b31bde08c0f76195ad2d93c0dc5bdf22/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 934f215 Nov 6, 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.4.1/linux-arm64/develop-934f2157b31bde08c0f76195ad2d93c0dc5bdf22/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 934f215 Nov 6, 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.4.1/darwin-x64/develop-934f2157b31bde08c0f76195ad2d93c0dc5bdf22/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 934f215 Nov 6, 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.4.1/darwin-arm64/develop-934f2157b31bde08c0f76195ad2d93c0dc5bdf22/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 934f215 Nov 6, 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.4.1/win32-x64/develop-934f2157b31bde08c0f76195ad2d93c0dc5bdf22/cypress.tgz

Please sign in to comment.