Skip to content

Commit 934f215

Browse files
chore: add after:browser:launch node event (#28180)
1 parent eab1730 commit 934f215

19 files changed

+326
-71
lines changed

cli/types/cypress.d.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -6009,7 +6009,11 @@ declare namespace Cypress {
60096009
(fn: (currentSubject: Subject) => void): Chainable<Subject>
60106010
}
60116011

6012-
interface BrowserLaunchOptions {
6012+
interface AfterBrowserLaunchDetails {
6013+
webSocketDebuggerUrl: string
6014+
}
6015+
6016+
interface BeforeBrowserLaunchOptions {
60136017
extensions: string[]
60146018
preferences: { [key: string]: any }
60156019
args: string[]
@@ -6090,12 +6094,13 @@ declare namespace Cypress {
60906094
}
60916095

60926096
interface PluginEvents {
6097+
(action: 'after:browser:launch', fn: (browser: Browser, browserLaunchDetails: AfterBrowserLaunchDetails) => void | Promise<void>): void
60936098
(action: 'after:run', fn: (results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult) => void | Promise<void>): void
60946099
(action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise<AfterScreenshotReturnObject>): void
60956100
(action: 'after:spec', fn: (spec: Spec, results: CypressCommandLine.RunResult) => void | Promise<void>): void
60966101
(action: 'before:run', fn: (runDetails: BeforeRunDetails) => void | Promise<void>): void
60976102
(action: 'before:spec', fn: (spec: Spec) => void | Promise<void>): void
6098-
(action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise<BrowserLaunchOptions>): void
6103+
(action: 'before:browser:launch', fn: (browser: Browser, afterBrowserLaunchOptions: BeforeBrowserLaunchOptions) => void | Promise<void> | BeforeBrowserLaunchOptions | Promise<BeforeBrowserLaunchOptions>): void
60996104
(action: 'file:preprocessor', fn: (file: FileObject) => string | Promise<string>): void
61006105
(action: 'dev-server:start', fn: (file: DevServerConfig) => Promise<ResolvedDevServerConfig>): void
61016106
(action: 'task', tasks: Tasks): void

packages/server/lib/browsers/browser-cri-client.ts

+7
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,13 @@ export class BrowserCriClient {
574574
this.extraTargetClients.delete(targetId)
575575
}
576576

577+
/**
578+
* @returns the websocket debugger URL for the currently connected browser
579+
*/
580+
getWebSocketDebuggerUrl () {
581+
return this.versionInfo.webSocketDebuggerUrl
582+
}
583+
577584
/**
578585
* Closes the browser client socket as well as the socket for the currently attached page target
579586
*/

packages/server/lib/browsers/chrome.ts

+4
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ export = {
645645

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

648+
await utils.executeAfterBrowserLaunch(browser, {
649+
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
650+
})
651+
648652
// return the launched browser process
649653
// with additional method to close the remote connection
650654
return launchedBrowser

packages/server/lib/browsers/cri-client.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -265,15 +265,26 @@ export const create = async ({
265265

266266
maybeDebugCdpMessages(cri)
267267

268-
// 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
269-
if (!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
268+
// Having a host set indicates that this is the child cri target, a.k.a.
269+
// the main Cypress tab (as opposed to the root browser cri target)
270+
const isChildTarget = !!host
271+
272+
// don't reconnect in these circumstances
273+
if (
274+
// is a child target. we only need to reconnect the root browser target
275+
!isChildTarget
276+
// running cypress in cypress - there are a lot of disconnects that happen
277+
// that we don't want to reconnect on
278+
&& !process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF
279+
) {
270280
cri.on('disconnect', retryReconnect)
271281
}
272282

273-
// 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.
274-
// Browser cri traffic is handled in browser-cri-client.ts. The basic approach here is we attach to targets and enable network traffic
275-
// We must attach in a paused state so that we can enable network traffic before the target starts running.
276-
if (host) {
283+
// We're only interested in child target traffic. Browser cri traffic is
284+
// handled in browser-cri-client.ts. The basic approach here is we attach
285+
// to targets and enable network traffic. We must attach in a paused state
286+
// so that we can enable network traffic before the target starts running.
287+
if (isChildTarget) {
277288
cri.on('Target.targetCrashed', async (event) => {
278289
if (event.targetId !== target) {
279290
return

packages/server/lib/browsers/electron.ts

+4
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ export = {
539539
},
540540
}) as BrowserInstance
541541

542+
await utils.executeAfterBrowserLaunch(browser, {
543+
webSocketDebuggerUrl: browserCriClient!.getWebSocketDebuggerUrl(),
544+
})
545+
542546
return instance
543547
},
544548
}

packages/server/lib/browsers/firefox.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ toolbar {
347347
348348
`
349349

350-
let browserCriClient
350+
let browserCriClient: BrowserCriClient | undefined
351351

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

384384
export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) {
385-
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient)
385+
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient!)
386386
}
387387

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

574574
return originalBrowserKill.apply(browserInstance, args)
575575
}
576+
577+
await utils.executeAfterBrowserLaunch(browser, {
578+
webSocketDebuggerUrl: browserCriClient.getWebSocketDebuggerUrl(),
579+
})
576580
} catch (err) {
577581
errors.throwErr('FIREFOX_COULD_NOT_CONNECT', err)
578582
}

packages/server/lib/browsers/utils.ts

+24
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as plugins from '../plugins'
77
import { getError } from '@packages/errors'
88
import * as launcher from '@packages/launcher'
99
import type { Automation } from '../automation'
10+
import type { Browser } from './types'
1011
import type { CriClient } from './cri-client'
1112

1213
declare global {
@@ -157,6 +158,27 @@ async function executeBeforeBrowserLaunch (browser, launchOptions: typeof defaul
157158
return launchOptions
158159
}
159160

161+
interface AfterBrowserLaunchDetails {
162+
webSocketDebuggerUrl: string | never
163+
}
164+
165+
async function executeAfterBrowserLaunch (browser: Browser, options: AfterBrowserLaunchDetails) {
166+
if (plugins.has('after:browser:launch')) {
167+
const span = telemetry.startSpan({ name: 'lifecycle:after:browser:launch' })
168+
169+
span?.setAttribute({
170+
name: browser.name,
171+
channel: browser.channel,
172+
version: browser.version,
173+
isHeadless: browser.isHeadless,
174+
})
175+
176+
await plugins.execute('after:browser:launch', browser, options)
177+
178+
span?.end()
179+
}
180+
}
181+
160182
function extendLaunchOptionsFromPlugins (launchOptions, pluginConfigResult, options: BrowserLaunchOpts) {
161183
// if we returned an array from the plugin
162184
// then we know the user is using the deprecated
@@ -423,6 +445,8 @@ export = {
423445

424446
extendLaunchOptionsFromPlugins,
425447

448+
executeAfterBrowserLaunch,
449+
426450
executeBeforeBrowserLaunch,
427451

428452
defaultLaunchOptions,

packages/server/lib/browsers/webkit.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
101101

102102
removeBadExitListener()
103103

104-
const pwBrowser = await pw.webkit.connect(pwServer.wsEndpoint())
104+
const websocketUrl = pwServer.wsEndpoint()
105+
const pwBrowser = await pw.webkit.connect(websocketUrl)
105106

106107
wkAutomation = await WebKitAutomation.create({
107108
automation,
@@ -147,5 +148,9 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc
147148
}
148149
}
149150

151+
await utils.executeAfterBrowserLaunch(browser, {
152+
webSocketDebuggerUrl: websocketUrl,
153+
})
154+
150155
return new WkInstance()
151156
}

packages/server/lib/plugins/child/browser_launch.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const util = require('../util')
33
const ARRAY_METHODS = ['concat', 'push', 'unshift', 'slice', 'pop', 'shift', 'slice', 'splice', 'filter', 'map', 'forEach', 'reduce', 'reverse', 'splice', 'includes']
44

55
module.exports = {
6-
wrap (ipc, invoke, ids, args) {
6+
wrapBefore (ipc, invoke, ids, args) {
77
// TODO: remove in next breaking release
88
// This will send a warning message when a deprecated API is used
99
// define array-like functions on this object so we can warn about using deprecated array API

packages/server/lib/plugins/child/run_plugins.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ class RunPlugins {
169169
case '_get:task:body':
170170
return this.taskGetBody(ids, args)
171171
case 'before:browser:launch':
172-
return browserLaunch.wrap(this.ipc, this.invoke, ids, args)
172+
return browserLaunch.wrapBefore(this.ipc, this.invoke, ids, args)
173+
case 'after:browser:launch':
174+
return util.wrapChildPromise(this.ipc, this.invoke, ids, args)
173175
default:
174176
debug('unexpected execute message:', event, args)
175177

packages/server/lib/plugins/child/validate_event.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const eventValidators = {
2727
'_get:task:body': isFunction,
2828
'_get:task:keys': isFunction,
2929
'_process:cross:origin:callback': isFunction,
30+
'after:browser:launch': isFunction,
3031
'after:run': isFunction,
3132
'after:screenshot': isFunction,
3233
'after:spec': isFunction,
@@ -42,7 +43,11 @@ const validateEvent = (event, handler, config, errConstructorFn) => {
4243
const validator = eventValidators[event]
4344

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

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

packages/server/test/integration/cypress_spec.js

+2
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,7 @@ describe('lib/cypress', () => {
10061006
ensureMinimumProtocolVersion: sinon.stub().resolves(),
10071007
attachToTargetUrl: sinon.stub().resolves(criClient),
10081008
close: sinon.stub().resolves(),
1009+
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
10091010
}
10101011

10111012
const cdpAutomation = {
@@ -1076,6 +1077,7 @@ describe('lib/cypress', () => {
10761077
attachToTargetUrl: sinon.stub().resolves(criClient),
10771078
currentlyAttachedTarget: criClient,
10781079
close: sinon.stub().resolves(),
1080+
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
10791081
}
10801082

10811083
sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient)

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

+28-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('lib/browsers/chrome', () => {
3333
attachToTargetUrl: sinon.stub().resolves(this.pageCriClient),
3434
close: sinon.stub().resolves(),
3535
ensureMinimumProtocolVersion: sinon.stub().withArgs('1.3').resolves(),
36+
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
3637
}
3738

3839
this.automation = {
@@ -93,14 +94,14 @@ describe('lib/browsers/chrome', () => {
9394
})
9495
})
9596

96-
it('is noop without before:browser:launch', function () {
97+
it('executeBeforeBrowserLaunch is noop if before:browser:launch is not registered', function () {
9798
return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)
9899
.then(() => {
99-
expect(plugins.execute).not.to.be.called
100+
expect(plugins.execute).not.to.be.calledWith('before:browser:launch')
100101
})
101102
})
102103

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

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

308+
it('sends after:browser:launch with debugger url', function () {
309+
const args = []
310+
const browser = { isHeadless: true }
311+
312+
sinon.stub(chrome, '_getArgs').returns(args)
313+
sinon.stub(plugins, 'has').returns(true)
314+
315+
plugins.execute.resolves(null)
316+
317+
return chrome.open(browser, 'http://', openOpts, this.automation)
318+
.then(() => {
319+
expect(plugins.execute).to.be.calledWith('after:browser:launch', browser, {
320+
webSocketDebuggerUrl: 'ws://debugger',
321+
})
322+
})
323+
})
324+
325+
it('executeAfterBrowserLaunch is noop if after:browser:launch is not registered', function () {
326+
return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)
327+
.then(() => {
328+
expect(plugins.execute).not.to.be.calledWith('after:browser:launch')
329+
})
330+
})
331+
307332
describe('downloads', function () {
308333
it('pushes create:download after download begins', function () {
309334
const downloadData = {

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

+30-4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe('lib/browsers/electron', () => {
8080
attachToTargetUrl: sinon.stub().resolves(this.pageCriClient),
8181
currentlyAttachedTarget: this.pageCriClient,
8282
close: sinon.stub().resolves(),
83+
getWebSocketDebuggerUrl: sinon.stub().returns('ws://debugger'),
8384
}
8485

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

113114
context('.open', () => {
114-
beforeEach(function () {
115-
return this.stubForOpen()
115+
beforeEach(async function () {
116+
// shortcut to set the browserCriClient singleton variable
117+
await electron._getAutomation({}, { onError: () => {} }, {})
118+
119+
await this.stubForOpen()
116120
})
117121

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

155-
it('is noop when before:browser:launch yields null', function () {
159+
it('executeBeforeBrowserLaunch is noop when before:browser:launch yields null', function () {
156160
plugins.has.returns(true)
157161
plugins.execute.resolves(null)
158162

@@ -207,6 +211,25 @@ describe('lib/browsers/electron', () => {
207211
expect(Windows.removeAllExtensions).to.be.calledTwice
208212
})
209213
})
214+
215+
it('sends after:browser:launch with debugger url', function () {
216+
plugins.has.returns(true)
217+
plugins.execute.resolves(null)
218+
219+
return electron.open('electron', this.url, this.options, this.automation)
220+
.then(() => {
221+
expect(plugins.execute).to.be.calledWith('after:browser:launch', 'electron', {
222+
webSocketDebuggerUrl: 'ws://debugger',
223+
})
224+
})
225+
})
226+
227+
it('executeAfterBrowserLaunch is noop if after:browser:launch is not registered', function () {
228+
return electron.open('electron', this.url, this.options, this.automation)
229+
.then(() => {
230+
expect(plugins.execute).not.to.be.calledWith('after:browser:launch')
231+
})
232+
})
210233
})
211234

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

824-
it('adds pid of new BrowserWindow to allPids list', function () {
847+
it('adds pid of new BrowserWindow to allPids list', async function () {
848+
// shortcut to set the browserCriClient singleton variable
849+
await electron._getAutomation({}, { onError: () => {} }, {})
850+
825851
const opts = electron._defaultOptions(this.options.projectRoot, this.state, this.options)
826852

827853
const NEW_WINDOW_PID = ELECTRON_PID * 2

0 commit comments

Comments
 (0)