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 authored and cacieprins committed Nov 7, 2023
1 parent 7a49d2a commit 5ef0b61
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 5ef0b61

Please sign in to comment.