diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index cf0604f27902..f4c69ec165cf 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -6009,7 +6009,11 @@ declare namespace Cypress { (fn: (currentSubject: Subject) => void): Chainable } - interface BrowserLaunchOptions { + interface AfterBrowserLaunchDetails { + webSocketDebuggerUrl: string + } + + interface BeforeBrowserLaunchOptions { extensions: string[] preferences: { [key: string]: any } args: string[] @@ -6090,12 +6094,13 @@ declare namespace Cypress { } interface PluginEvents { + (action: 'after:browser:launch', fn: (browser: Browser, browserLaunchDetails: AfterBrowserLaunchDetails) => void | Promise): void (action: 'after:run', fn: (results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult) => void | Promise): void (action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise): void (action: 'after:spec', fn: (spec: Spec, results: CypressCommandLine.RunResult) => void | Promise): void (action: 'before:run', fn: (runDetails: BeforeRunDetails) => void | Promise): void (action: 'before:spec', fn: (spec: Spec) => void | Promise): void - (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise): void + (action: 'before:browser:launch', fn: (browser: Browser, afterBrowserLaunchOptions: BeforeBrowserLaunchOptions) => void | Promise | BeforeBrowserLaunchOptions | Promise): void (action: 'file:preprocessor', fn: (file: FileObject) => string | Promise): void (action: 'dev-server:start', fn: (file: DevServerConfig) => Promise): void (action: 'task', tasks: Tasks): void diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index c7f0964231ca..8d11b913afed 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -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 */ diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 8ff81eab95cd..62026997c306 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -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 diff --git a/packages/server/lib/browsers/cri-client.ts b/packages/server/lib/browsers/cri-client.ts index 97325b755cac..a578592a4a53 100644 --- a/packages/server/lib/browsers/cri-client.ts +++ b/packages/server/lib/browsers/cri-client.ts @@ -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 diff --git a/packages/server/lib/browsers/electron.ts b/packages/server/lib/browsers/electron.ts index fd4f819b4c8c..fb9ebf451564 100644 --- a/packages/server/lib/browsers/electron.ts +++ b/packages/server/lib/browsers/electron.ts @@ -539,6 +539,10 @@ export = { }, }) as BrowserInstance + await utils.executeAfterBrowserLaunch(browser, { + webSocketDebuggerUrl: browserCriClient!.getWebSocketDebuggerUrl(), + }) + return instance }, } diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index 4fbbc2c9297d..e7ae8e63ecd7 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -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 @@ -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 () { @@ -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) } diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index b6f246b28ff9..deed00e225cc 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -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 { @@ -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 @@ -423,6 +445,8 @@ export = { extendLaunchOptionsFromPlugins, + executeAfterBrowserLaunch, + executeBeforeBrowserLaunch, defaultLaunchOptions, diff --git a/packages/server/lib/browsers/webkit.ts b/packages/server/lib/browsers/webkit.ts index a02540daba39..26955261f4e5 100644 --- a/packages/server/lib/browsers/webkit.ts +++ b/packages/server/lib/browsers/webkit.ts @@ -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, @@ -147,5 +148,9 @@ export async function open (browser: Browser, url: string, options: BrowserLaunc } } + await utils.executeAfterBrowserLaunch(browser, { + webSocketDebuggerUrl: websocketUrl, + }) + return new WkInstance() } diff --git a/packages/server/lib/plugins/child/browser_launch.js b/packages/server/lib/plugins/child/browser_launch.js index c5680961e3d8..54b777d384e3 100644 --- a/packages/server/lib/plugins/child/browser_launch.js +++ b/packages/server/lib/plugins/child/browser_launch.js @@ -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 diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index f86a35d5cdf9..0e439519860b 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -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) diff --git a/packages/server/lib/plugins/child/validate_event.js b/packages/server/lib/plugins/child/validate_event.js index 0a82f902d7d8..2c8832682552 100644 --- a/packages/server/lib/plugins/child/validate_event.js +++ b/packages/server/lib/plugins/child/validate_event.js @@ -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, @@ -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}`) diff --git a/packages/server/test/integration/cypress_spec.js b/packages/server/test/integration/cypress_spec.js index ca48af7aae03..f7957c35a0cf 100644 --- a/packages/server/test/integration/cypress_spec.js +++ b/packages/server/test/integration/cypress_spec.js @@ -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 = { @@ -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) diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 20dc11dfe6d7..5f8f5286c436 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -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 = { @@ -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) @@ -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 = { diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index ecb0101d2f16..194e1414e076 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -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) @@ -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 () { @@ -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) @@ -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', () => { @@ -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 diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index b0ab02ffacd3..6827d10f0fca 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -126,7 +126,9 @@ describe('lib/browsers/firefox', () => { context('#open', () => { beforeEach(function () { - this.browser = { name: 'firefox', channel: 'stable' } + // majorVersion >= 86 indicates CDP support for Firefox, which provides + // the CDP debugger URL for the after:browser:launch tests + this.browser = { name: 'firefox', channel: 'stable', majorVersion: 100 } this.automation = { use: sinon.stub().returns({}), } @@ -150,31 +152,40 @@ describe('lib/browsers/firefox', () => { sinon.stub(plugins, 'execute') sinon.stub(launch, 'launch').returns(this.browserInstance) sinon.stub(utils, 'writeExtension').resolves('/path/to/ext') + sinon.stub(utils, 'getPort').resolves(1234) sinon.spy(FirefoxProfile.prototype, 'setPreference') sinon.spy(FirefoxProfile.prototype, 'updatePreferences') + sinon.spy(FirefoxProfile.prototype, 'path') + + const browserCriClient: BrowserCriClient = sinon.createStubInstance(BrowserCriClient) - return sinon.spy(FirefoxProfile.prototype, 'path') + browserCriClient.attachToTargetUrl = sinon.stub().resolves({}) + browserCriClient.getWebSocketDebuggerUrl = sinon.stub().returns('ws://debugger') + browserCriClient.close = sinon.stub().resolves() + + sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient) + sinon.stub(CdpAutomation, 'create').resolves() }) it('executes before:browser:launch if registered', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves(null) return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { - expect(plugins.execute).to.be.called + expect(plugins.execute).to.be.calledWith('before:browser:launch') }) }) it('does not execute before:browser:launch if not registered', function () { - plugins.has.returns(false) + plugins.has.withArgs('before:browser:launch').returns(false) return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { - expect(plugins.execute).not.to.be.called + expect(plugins.execute).not.to.be.calledWith('before:browser:launch') }) }) it('uses default preferences if before:browser:launch returns falsy value', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves(null) return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { @@ -183,7 +194,7 @@ describe('lib/browsers/firefox', () => { }) it('uses default preferences if before:browser:launch returns object with non-object preferences', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves({ preferences: [], }) @@ -194,7 +205,7 @@ describe('lib/browsers/firefox', () => { }) it('sets preferences if returned by before:browser:launch', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves({ preferences: { 'foo': 'bar' }, }) @@ -205,7 +216,7 @@ describe('lib/browsers/firefox', () => { }) it('adds extensions returned by before:browser:launch, along with cypress extension', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves({ extensions: ['/path/to/user/ext'], }) @@ -218,7 +229,7 @@ describe('lib/browsers/firefox', () => { }) it('adds only cypress extension if before:browser:launch returns object with non-array extensions', function () { - plugins.has.returns(true) + plugins.has.withArgs('before:browser:launch').returns(true) plugins.execute.resolves({ extensions: 'not-an-array', }) @@ -331,12 +342,13 @@ describe('lib/browsers/firefox', () => { it('launches with the url and args', function () { return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { - expect(launch.launch).to.be.calledWith(this.browser, 'about:blank', undefined, [ + expect(launch.launch).to.be.calledWith(this.browser, 'about:blank', 1234, [ '-marionette', '-new-instance', '-foreground', '-start-debugger-server', '-no-remote', + '--remote-debugging-port=1234', '-profile', '/path/to/appData/firefox-stable/interactive', ]) @@ -410,6 +422,25 @@ describe('lib/browsers/firefox', () => { }) }) + it('executes after:browser:launch if registered', function () { + plugins.has.withArgs('after:browser:launch').returns(true) + plugins.execute.resolves(null) + + return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { + expect(plugins.execute).to.be.calledWith('after:browser:launch', this.browser, { + webSocketDebuggerUrl: 'ws://debugger', + }) + }) + }) + + it('does not execute after:browser:launch if not registered', function () { + plugins.has.withArgs('after:browser:launch').returns(false) + + return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => { + expect(plugins.execute).not.to.be.calledWith('after:browser:launch') + }) + }) + context('returns BrowserInstance', function () { it('from browsers.launch', async function () { const instance = await firefox.open(this.browser, 'http://', this.options, this.automation) diff --git a/packages/server/test/unit/browsers/webkit_spec.ts b/packages/server/test/unit/browsers/webkit_spec.ts index 9906936c98a8..708224b6ecd7 100644 --- a/packages/server/test/unit/browsers/webkit_spec.ts +++ b/packages/server/test/unit/browsers/webkit_spec.ts @@ -1,12 +1,79 @@ -require('../../spec_helper') - +import { proxyquire } from '../../spec_helper' import { expect } from 'chai' +import utils from '../../../lib/browsers/utils' +import * as plugins from '../../../lib/plugins' -import * as webkit from '../../../lib/browsers/webkit' +function getWebkit (dependencies = {}) { + return proxyquire('../lib/browsers/webkit', dependencies) as typeof import('../../../lib/browsers/webkit') +} describe('lib/browsers/webkit', () => { + context('#open', () => { + let browser + let options + let automation + let webkit + + beforeEach(async () => { + browser = {} + options = { experimentalWebKitSupport: true } + automation = { use: sinon.stub() } + + const launchOptions = { + extensions: [], + args: [], + preferences: { }, + } + const pwWebkit = { + webkit: { + connect: sinon.stub().resolves({ + on: sinon.stub(), + }), + launchServer: sinon.stub().resolves({ + wsEndpoint: sinon.stub().returns('ws://debugger'), + process: sinon.stub().returns({ pid: 'pid' }), + }), + }, + } + const wkAutomation = { + WebKitAutomation: { + create: sinon.stub().resolves({}), + }, + } + + sinon.stub(utils, 'executeBeforeBrowserLaunch').resolves(launchOptions as any) + sinon.stub(plugins, 'execute').resolves() + sinon.stub(plugins, 'has') + + webkit = getWebkit({ + 'playwright-webkit': pwWebkit, + './webkit-automation': wkAutomation, + }) + }) + + it('sends after:browser:launch with debugger url', async () => { + (plugins.has as any).returns(true) + + await webkit.open(browser as any, 'http://the.url', options as any, automation as any) + + expect(plugins.execute).to.be.calledWith('after:browser:launch', browser, { + webSocketDebuggerUrl: 'ws://debugger', + }) + }) + + it('executeAfterBrowserLaunch is noop if after:browser:launch is not registered', async () => { + (plugins.has as any).returns(false) + + await webkit.open(browser as any, 'http://the.url', options as any, automation as any) + + expect(plugins.execute).not.to.be.calledWith('after:browser:launch') + }) + }) + context('#connectProtocolToBrowser', () => { it('throws error', () => { + const webkit = getWebkit() + expect(webkit.connectProtocolToBrowser).to.throw('Protocol is not yet supported in WebKit.') }) }) diff --git a/packages/server/test/unit/plugins/child/run_plugins_spec.js b/packages/server/test/unit/plugins/child/run_plugins_spec.js index f87555f27c0f..7b89e2ec7fa6 100644 --- a/packages/server/test/unit/plugins/child/run_plugins_spec.js +++ b/packages/server/test/unit/plugins/child/run_plugins_spec.js @@ -141,6 +141,7 @@ describe('lib/plugins/child/run_plugins', () => { describe(`on 'execute:plugins' message`, () => { let onFilePreprocessor + let afterBrowserLaunch let beforeBrowserLaunch let taskRequested let setupNodeEventsFn @@ -149,11 +150,13 @@ describe('lib/plugins/child/run_plugins', () => { sinon.stub(preprocessor, 'wrap') onFilePreprocessor = sinon.stub().resolves() + afterBrowserLaunch = sinon.stub().resolves() beforeBrowserLaunch = sinon.stub().resolves() taskRequested = sinon.stub().resolves('foo') setupNodeEventsFn = (on) => { on('file:preprocessor', onFilePreprocessor) + on('after:browser:launch', afterBrowserLaunch) on('before:browser:launch', beforeBrowserLaunch) on('task', taskRequested) } @@ -201,6 +204,35 @@ describe('lib/plugins/child/run_plugins', () => { ipc.on.withArgs('execute:plugins').yield('before:browser:launch', ids, args) }) + it('wraps child promise', () => { + expect(util.wrapChildPromise).to.be.calledWith(ipc, sinon.match.func, ids, args) + }) + + it('invokes registered function when invoked by handler', () => { + // console.log(util.wrapChildPromise.withArgs(ipc, sinon.match.func, ids, args).args) + util.wrapChildPromise.withArgs(ipc, sinon.match.func, ids, args).args[0][1](5, args) + + expect(beforeBrowserLaunch).to.be.calledWith(...args) + }) + }) + + context('after:browser:launch', () => { + let args + const ids = { eventId: 2, invocationId: '00' } + + beforeEach(async () => { + sinon.stub(util, 'wrapChildPromise') + + await runPlugins.runSetupNodeEvents(config, setupNodeEventsFn) + + const browser = {} + const launchOptions = browserUtils.getDefaultLaunchOptions({}) + + args = [browser, launchOptions] + + ipc.on.withArgs('execute:plugins').yield('after:browser:launch', ids, args) + }) + it('wraps child promise', () => { expect(util.wrapChildPromise).to.be.called expect(util.wrapChildPromise.lastCall.args[0]).to.equal(ipc) @@ -212,7 +244,7 @@ describe('lib/plugins/child/run_plugins', () => { it('invokes registered function when invoked by handler', () => { util.wrapChildPromise.lastCall.args[1](4, args) - expect(beforeBrowserLaunch).to.be.calledWith(...args) + expect(afterBrowserLaunch).to.be.calledWith(...args) }) }) diff --git a/packages/server/test/unit/plugins/child/validate_event_spec.js b/packages/server/test/unit/plugins/child/validate_event_spec.js index dee6e85a0727..f024ee79cbbb 100644 --- a/packages/server/test/unit/plugins/child/validate_event_spec.js +++ b/packages/server/test/unit/plugins/child/validate_event_spec.js @@ -4,6 +4,7 @@ const _ = require('lodash') const validateEvent = require('../../../../lib/plugins/child/validate_event') const events = [ + ['after:browser:launch', 'a function', () => {}], ['after:run', 'a function', () => {}], ['after:screenshot', 'a function', () => {}], ['after:spec', 'a function', () => {}], diff --git a/packages/server/test/unit/util/args_spec.js b/packages/server/test/unit/util/args_spec.js index 05ddb06210ec..ff7e1ba4af00 100644 --- a/packages/server/test/unit/util/args_spec.js +++ b/packages/server/test/unit/util/args_spec.js @@ -8,7 +8,7 @@ const minimist = require('minimist') const argsUtil = require(`../../../lib/util/args`) const getWindowsProxyUtil = require(`../../../lib/util/get-windows-proxy`) -const cwd = process.cwd() +const getCwd = () => process.cwd() describe('lib/util/args', () => { beforeEach(function () { @@ -92,7 +92,7 @@ describe('lib/util/args', () => { context('--project', () => { it('sets projectRoot', function () { - const projectRoot = path.resolve(cwd, './foo/bar') + const projectRoot = path.resolve(getCwd(), './foo/bar') const options = this.setup('--project', './foo/bar') expect(options.projectRoot).to.eq(projectRoot) @@ -113,7 +113,7 @@ describe('lib/util/args', () => { context('--run-project', () => { it('sets projectRoot', function () { - const projectRoot = path.resolve(cwd, '/baz') + const projectRoot = path.resolve(getCwd(), '/baz') const options = this.setup('--run-project', '/baz') expect(options.projectRoot).to.eq(projectRoot) @@ -138,16 +138,16 @@ describe('lib/util/args', () => { it('converts to array', function () { const options = this.setup('--run-project', 'foo', '--spec', 'cypress/integration/a.js,cypress/integration/b.js,cypress/integration/c.js') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/a.js`) - expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/b.js`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/a.js`) + expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/b.js`) - expect(options.spec[2]).to.eq(`${cwd}/cypress/integration/c.js`) + expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/c.js`) }) it('discards wrapping single quotes', function () { const options = this.setup('--run-project', 'foo', '--spec', '\'cypress/integration/foo_spec.js\'') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/foo_spec.js`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo_spec.js`) }) it('throws if argument cannot be parsed', function () { @@ -165,56 +165,56 @@ describe('lib/util/args', () => { it('should be correctly parsing globs with lists & ranges', function () { const options = this.setup('--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) }) it('should be correctly parsing globs with a mix of lists, ranges & regular paths', function () { const options = this.setup('--spec', 'cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js,cypress/integration/foo.spec.js') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) - expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/foo.spec.js`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{[!a]*.spec.js,sub1,{sub2,sub3/sub4}}/*.js`) + expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo.spec.js`) }) it('should be correctly parsing single glob with range', function () { const options = this.setup('--spec', 'cypress/integration/[a-c]*/**') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/[a-c]*/**`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/[a-c]*/**`) }) it('should be correctly parsing single glob with list', function () { const options = this.setup('--spec', 'cypress/integration/{a,b,c}/*.js') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/{a,b,c}/*.js`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/{a,b,c}/*.js`) }) // https://github.com/cypress-io/cypress/issues/20794 it('does not split at filename with glob pattern', function () { const options = this.setup('--spec', 'cypress/integration/foo/bar/[baz]/test.ts,cypress/integration/foo1/bar/[baz]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/foo/bar/[baz]/test.ts`) - expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/foo1/bar/[baz]/test.ts`) - expect(options.spec[2]).to.eq(`${cwd}/cypress/integration/foo2/bar/baz/test.ts`) - expect(options.spec[3]).to.eq(`${cwd}/cypress/integration/foo3/bar/baz/foo4.ts`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/[baz]/test.ts`) + expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/[baz]/test.ts`) + expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`) + expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`) }) // https://github.com/cypress-io/cypress/issues/20794 it('correctly splits at comma with glob pattern', function () { const options = this.setup('--spec', 'cypress/integration/foo/bar/baz/test.ts,cypress/integration/foo1/bar/[baz]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/foo/bar/baz/test.ts`) - expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/foo1/bar/[baz]/test.ts`) - expect(options.spec[2]).to.eq(`${cwd}/cypress/integration/foo2/bar/baz/test.ts`) - expect(options.spec[3]).to.eq(`${cwd}/cypress/integration/foo3/bar/baz/foo4.ts`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/baz/test.ts`) + expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/[baz]/test.ts`) + expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`) + expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`) }) // https://github.com/cypress-io/cypress/issues/20794 it('correctly splits at comma with escaped glob pattern', function () { const options = this.setup('--spec', 'cypress/integration/foo/bar/\[baz\]/test.ts,cypress/integration/foo1/bar/\[baz1\]/test.ts,cypress/integration/foo2/bar/baz/test.ts,cypress/integration/foo3/bar/baz/foo4.ts') - expect(options.spec[0]).to.eq(`${cwd}/cypress/integration/foo/bar/\[baz\]/test.ts`) - expect(options.spec[1]).to.eq(`${cwd}/cypress/integration/foo1/bar/\[baz1\]/test.ts`) - expect(options.spec[2]).to.eq(`${cwd}/cypress/integration/foo2/bar/baz/test.ts`) - expect(options.spec[3]).to.eq(`${cwd}/cypress/integration/foo3/bar/baz/foo4.ts`) + expect(options.spec[0]).to.eq(`${getCwd()}/cypress/integration/foo/bar/\[baz\]/test.ts`) + expect(options.spec[1]).to.eq(`${getCwd()}/cypress/integration/foo1/bar/\[baz1\]/test.ts`) + expect(options.spec[2]).to.eq(`${getCwd()}/cypress/integration/foo2/bar/baz/test.ts`) + expect(options.spec[3]).to.eq(`${getCwd()}/cypress/integration/foo3/bar/baz/foo4.ts`) }) }) @@ -600,9 +600,9 @@ describe('lib/util/args', () => { this.hosts = { a: 'b', b: 'c' } this.blockHosts = ['a.com', 'b.com'] this.specs = [ - path.join(cwd, 'foo'), - path.join(cwd, 'bar'), - path.join(cwd, 'baz'), + path.join(getCwd(), 'foo'), + path.join(getCwd(), 'bar'), + path.join(getCwd(), 'baz'), ] this.env = { @@ -643,7 +643,7 @@ describe('lib/util/args', () => { it('backs up env, config, reporterOptions, spec', function () { expect(this.obj).to.deep.eq({ - cwd, + cwd: getCwd(), _: [], config: this.config, invokedFromCli: false, @@ -666,12 +666,12 @@ describe('lib/util/args', () => { expect(args).to.deep.eq([ `--config=${mergedConfig}`, - `--cwd=${cwd}`, + `--cwd=${getCwd()}`, `--spec=${JSON.stringify(this.specs)}`, ]) expect(argsUtil.toObject(args)).to.deep.eq({ - cwd, + cwd: getCwd(), _: [], invokedFromCli: true, config: this.config, @@ -684,7 +684,7 @@ describe('lib/util/args', () => { expect(result).to.deep.equal({ ciBuildId: '1e100', - cwd, + cwd: getCwd(), _: [], invokedFromCli: false, config: {}, @@ -695,7 +695,7 @@ describe('lib/util/args', () => { const result = argsUtil.toObject(['--config', '{"baseUrl": "http://foobar.com", "specPattern":"**/*.test.js"}']) expect(result).to.deep.equal({ - cwd, + cwd: getCwd(), _: [], invokedFromCli: false, config: { @@ -718,7 +718,7 @@ describe('lib/util/args', () => { ] expect(argsUtil.toObject(argv)).to.deep.eq({ - cwd, + cwd: getCwd(), _: [ '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress', '/Applications/Cypress.app', @@ -743,7 +743,7 @@ describe('lib/util/args', () => { ] expect(argsUtil.toObject(argv)).to.deep.eq({ - cwd, + cwd: getCwd(), _: [ '/private/var/folders/wr/3xdzqnq16lz5r1j_xtl443580000gn/T/cypress/Cypress.app/Contents/MacOS/Cypress', '/Applications/Cypress.app1',