diff --git a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts index a7347a6a9685..6bb825839dea 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts @@ -144,7 +144,7 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: expect(ctx.actions.browser.setActiveBrowserById).to.have.been.calledWith(browserId) expect(genId).to.eql('firefox-firefox-stable') expect(ctx.actions.project.launchProject).to.have.been.calledWith( - ctx.coreData.currentTestingType, {}, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')), + ctx.coreData.currentTestingType, undefined, o.sinon.match(new RegExp('cypress\-in\-cypress\/src\/TestComponent\.spec\.jsx$')), ) }) }) diff --git a/packages/app/cypress/e2e/top-nav.cy.ts b/packages/app/cypress/e2e/top-nav.cy.ts index f52bf0bf2ad9..871c9186595b 100644 --- a/packages/app/cypress/e2e/top-nav.cy.ts +++ b/packages/app/cypress/e2e/top-nav.cy.ts @@ -110,7 +110,7 @@ describe('App Top Nav Workflows', () => { expect(ctx.actions.browser.setActiveBrowserById).to.have.been.calledWith(browserId) expect(genId).to.eql('edge-chromium-stable') expect(ctx.actions.project.launchProject).to.have.been.calledWith( - ctx.coreData.currentTestingType, {}, undefined, + ctx.coreData.currentTestingType, undefined, undefined, ) }) }) diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index cc8b32e74d98..3e0eae11c1de 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -1,5 +1,5 @@ import type { CodeGenType, MutationSetProjectPreferencesInGlobalCacheArgs, NexusGenObjects, NexusGenUnions } from '@packages/graphql/src/gen/nxs.gen' -import type { InitializeProjectOptions, FoundBrowser, FoundSpec, LaunchOpts, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig, AllowedState, SpecWithRelativeRoot } from '@packages/types' +import type { InitializeProjectOptions, FoundBrowser, FoundSpec, OpenProjectLaunchOptions, Preferences, TestingType, ReceivedCypressOptions, AddProject, FullConfig, AllowedState, SpecWithRelativeRoot, OpenProjectLaunchOpts } from '@packages/types' import type { EventEmitter } from 'events' import execa from 'execa' import path from 'path' @@ -22,7 +22,7 @@ export interface ProjectApiShape { * order for CT to startup */ openProjectCreate(args: InitializeProjectOptions, options: OpenProjectLaunchOptions): Promise - launchProject(browser: FoundBrowser, spec: Cypress.Spec, options: LaunchOpts): Promise + launchProject(browser: FoundBrowser, spec: Cypress.Spec, options?: OpenProjectLaunchOpts): Promise insertProjectToCache(projectRoot: string): Promise removeProjectFromCache(projectRoot: string): Promise getProjectRootsFromCache(): Promise @@ -175,7 +175,7 @@ export class ProjectActions { // When switching testing type, the project should be relaunched in the previously selected browser if (this.ctx.coreData.app.relaunchBrowser) { this.ctx.project.setRelaunchBrowser(false) - await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType, {}) + await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType) } }) } catch (e) { @@ -228,7 +228,7 @@ export class ProjectActions { } } - async launchProject (testingType: Cypress.TestingType | null, options: LaunchOpts, specPath?: string | null) { + async launchProject (testingType: Cypress.TestingType | null, options?: OpenProjectLaunchOpts, specPath?: string | null) { if (!this.ctx.currentProject) { return null } diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index c414b6cd1707..db8d09eca653 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -287,7 +287,7 @@ export class ProjectLifecycleManager { if (this.ctx.coreData.activeBrowser) { // if `cypress open` was launched with a `--project` and `--testingType`, go ahead and launch the `--browser` if (this.ctx.modeOptions.project && this.ctx.modeOptions.testingType) { - await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType, {}) + await this.ctx.actions.project.launchProject(this.ctx.coreData.currentTestingType) } return diff --git a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts index d9ddffbcf3cd..719b60e498ae 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eSupport.ts @@ -294,6 +294,7 @@ function startAppServer (mode: 'component' | 'e2e' = 'e2e', options: { skipMocki if (!ctx.lifecycleManager.browsers?.length) throw new Error('No browsers available in startAppServer') await ctx.actions.browser.setActiveBrowser(ctx.lifecycleManager.browsers[0]) + // @ts-expect-error this interface is strict about the options it expects await ctx.actions.project.launchProject(o.mode, { url: o.url }) if (!o.skipMockingPrompts diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 8c3f4a9770f2..b5586ca75236 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -290,7 +290,7 @@ export const mutation = mutationType({ specPath: stringArg(), }, resolve: async (_, args, ctx) => { - await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, {}, args.specPath) + await ctx.actions.project.launchProject(ctx.coreData.currentTestingType, undefined, args.specPath) return ctx.lifecycleManager }, diff --git a/packages/server/lib/browsers/browser-cri-client.ts b/packages/server/lib/browsers/browser-cri-client.ts index 0e03bf03f469..2c6bc5e6cfbb 100644 --- a/packages/server/lib/browsers/browser-cri-client.ts +++ b/packages/server/lib/browsers/browser-cri-client.ts @@ -2,7 +2,7 @@ import CRI from 'chrome-remote-interface' import Debug from 'debug' import { _connectAsync, _getDelayMsForRetry } from './protocol' import * as errors from '../errors' -import { create, CRIWrapper } from './cri-client' +import { create, CriClient } from './cri-client' const HOST = '127.0.0.1' @@ -67,8 +67,8 @@ const retryWithIncreasingDelay = async (retryable: () => Promise, browserN } export class BrowserCriClient { - currentlyAttachedTarget: CRIWrapper.Client | undefined - private constructor (private browserClient: CRIWrapper.Client, private versionInfo, private port: number, private browserName: string, private onAsynchronousError: Function) {} + currentlyAttachedTarget: CriClient | undefined + private constructor (private browserClient: CriClient, private versionInfo, private port: number, private browserName: string, private onAsynchronousError: Function) {} /** * Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the @@ -79,7 +79,7 @@ export class BrowserCriClient { * @param onAsynchronousError callback for any cdp fatal errors * @returns a wrapper around the chrome remote interface that is connected to the browser target */ - static async create (port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CRIWrapper.Client) => void): Promise { + static async create (port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void): Promise { await ensureLiveBrowser(port, browserName) return retryWithIncreasingDelay(async () => { @@ -110,7 +110,7 @@ export class BrowserCriClient { * @param url the url to attach to * @returns the chrome remote interface wrapper for the target */ - attachToTargetUrl = async (url: string): Promise => { + attachToTargetUrl = async (url: string): Promise => { // Continue trying to re-attach until succcessful. // If the browser opens slowly, this will fail until // The browser and automation API is ready, so we try a few diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index 77d7872827ad..df00b00d059d 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -3,6 +3,7 @@ import _ from 'lodash' import Bluebird from 'bluebird' import type { Protocol } from 'devtools-protocol' +import type ProtocolMapping from 'devtools-protocol/types/protocol-mapping' import { cors, uri } from '@packages/network' import debugModule from 'debug' import { URL } from 'url' @@ -10,6 +11,10 @@ import { URL } from 'url' import type { Automation } from '../automation' import type { ResourceType, BrowserPreRequest, BrowserResponseReceived } from '@packages/proxy' +export type CdpCommand = keyof ProtocolMapping.Commands + +export type CdpEvent = keyof ProtocolMapping.Events + const debugVerbose = debugModule('cypress-verbose:server:browsers:cdp_automation') export type CyCookie = Pick & { @@ -163,9 +168,9 @@ export const normalizeResourceType = (resourceType: string | undefined): Resourc return ffToStandardResourceTypeMap[resourceType] || 'other' } -type SendDebuggerCommand = (message: string, data?: any) => Promise -type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise -type OnFn = (eventName: string, cb: Function) => void +type SendDebuggerCommand = (message: CdpCommand, data?: any) => Promise +type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise | void +type OnFn = (eventName: CdpEvent, cb: Function) => void // the intersection of what's valid in CDP and what's valid in FFCDP // Firefox: https://searchfox.org/mozilla-central/rev/98a9257ca2847fad9a19631ac76199474516b31e/remote/cdp/domains/parent/Network.jsm#22 diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 7d350c3fb8a7..c2500b1ee230 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -18,11 +18,9 @@ import utils from './utils' import type { Browser } from './types' import { BrowserCriClient } from './browser-cri-client' import type { LaunchedBrowser } from '@packages/launcher/lib/browsers' -import type { CRIWrapper } from './cri-client' +import type { CriClient } from './cri-client' import type { Automation } from '../automation' - -// TODO: this is defined in `cypress-npm-api` but there is currently no way to get there -type CypressConfiguration = any +import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types' const debug = debugModule('cypress:server:browsers:chrome') @@ -123,7 +121,7 @@ const DEFAULT_ARGS = [ '--disable-dev-shm-usage', ] -let browserCriClient +let browserCriClient: BrowserCriClient | undefined /** * Reads all known preference files (CHROME_PREFERENCE_PATHS) from disk and retur @@ -320,7 +318,7 @@ const _handleDownloads = async function (client, dir, automation) { let frameTree let gettingFrameTree -const onReconnect = (client: CRIWrapper.Client) => { +const onReconnect = (client: CriClient) => { // if the client disconnects (e.g. due to a computer sleeping), update // the frame tree on reconnect in cases there were changes while // the client was disconnected @@ -328,7 +326,7 @@ const onReconnect = (client: CRIWrapper.Client) => { } // eslint-disable-next-line @cypress/dev/arrow-body-multiline-braces -const _updateFrameTree = (client: CRIWrapper.Client, eventName) => async () => { +const _updateFrameTree = (client: CriClient, eventName) => async () => { debug(`update frame tree for ${eventName}`) gettingFrameTree = new Promise(async (resolve) => { @@ -433,8 +431,8 @@ const _handlePausedRequests = async (client) => { }) } -const _setAutomation = async (client: CRIWrapper.Client, automation: Automation, resetBrowserTargets: (shouldKeepTabOpen: boolean) => Promise, options: CypressConfiguration = {}) => { - const cdpAutomation = await CdpAutomation.create(client.send, client.on, resetBrowserTargets, automation, options.experimentalSessionAndOrigin) +const _setAutomation = async (client: CriClient, automation: Automation, resetBrowserTargets: (shouldKeepTabOpen: boolean) => Promise, options: BrowserLaunchOpts) => { + const cdpAutomation = await CdpAutomation.create(client.send, client.on, resetBrowserTargets, automation, !!options.experimentalSessionAndOrigin) return automation.use(cdpAutomation) } @@ -490,7 +488,7 @@ export = { return extensionDest }, - _getArgs (browser: Browser, options: CypressConfiguration, port: string) { + _getArgs (browser: Browser, options: BrowserLaunchOpts, port: string) { const args = ([] as string[]).concat(DEFAULT_ARGS) if (os.platform() === 'linux') { @@ -551,25 +549,52 @@ export = { return args }, - async connectToNewSpec (browser: Browser, options: CypressConfiguration = {}, automation: Automation) { + 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 }) const browserCriClient = this._getBrowserCriClient() + + if (!browserCriClient) throw new Error('Missing browserCriClient in connectToNewSpec') + const pageCriClient = browserCriClient.currentlyAttachedTarget + if (!pageCriClient) throw new Error('Missing pageCriClient in connectToNewSpec') + + if (!options.url) throw new Error('Missing url in connectToNewSpec') + + await this.attachListeners(browser, options.url, pageCriClient, automation, options) + }, + + async connectToExisting (browser: Browser, options: BrowserLaunchOpts, automation) { + const port = await protocol.getRemoteDebuggingPort() + + debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port }) + if (!options.onError) throw new Error('Missing onError in connectToExisting') + + const browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect) + + if (!options.url) throw new Error('Missing url in connectToExisting') + + const pageCriClient = await browserCriClient.attachToTargetUrl(options.url) + + await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options) + }, + + async attachListeners (browser: Browser, url: string, pageCriClient, automation: Automation, options: BrowserLaunchOpts & { onInitializeNewBrowserTab?: () => void }) { + if (!browserCriClient) throw new Error('Missing browserCriClient in attachListeners') + await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options) - // make sure page events are re enabled or else frame tree updates will NOT work as well as other items listening for page events await pageCriClient.send('Page.enable') - await options.onInitializeNewBrowserTab() + await options.onInitializeNewBrowserTab?.() await Promise.all([ this._maybeRecordVideo(pageCriClient, options, browser.majorVersion), this._handleDownloads(pageCriClient, options.downloadsFolder, automation), ]) - await this._navigateUsingCRI(pageCriClient, options.url) + await this._navigateUsingCRI(pageCriClient, url) if (options.experimentalSessionAndOrigin) { await this._handlePausedRequests(pageCriClient) @@ -577,17 +602,7 @@ export = { } }, - async connectToExisting (browser: Browser, options: CypressConfiguration = {}, automation) { - const port = await protocol.getRemoteDebuggingPort() - - debug('connecting to existing chrome instance with url and debugging port', { url: options.url, port }) - const browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect) - const pageCriClient = await browserCriClient.attachToTargetUrl(options.url) - - await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options) - }, - - async open (browser: Browser, url, options: CypressConfiguration = {}, automation: Automation): Promise { + async open (browser: Browser, url, options: BrowserLaunchOpts, automation: Automation): Promise { const { isTextTerminal } = options const userDir = utils.getProfileDir(browser, isTextTerminal) @@ -646,6 +661,8 @@ export = { // SECOND connect to the Chrome remote interface // and when the connection is ready // navigate to the actual url + if (!options.onError) throw new Error('Missing onError in chrome#open') + browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError, onReconnect) la(browserCriClient, 'expected Chrome remote interface reference', browserCriClient) @@ -669,7 +686,7 @@ export = { debug('closing remote interface client') // Do nothing on failure here since we're shutting down anyway - browserCriClient.close().catch() + browserCriClient?.close().catch() browserCriClient = undefined debug('closing chrome') @@ -679,21 +696,7 @@ export = { const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank') - await this._setAutomation(pageCriClient, automation, browserCriClient.resetBrowserTargets, options) - - await pageCriClient.send('Page.enable') - - await Promise.all([ - this._maybeRecordVideo(pageCriClient, options, browser.majorVersion), - this._handleDownloads(pageCriClient, options.downloadsFolder, automation), - ]) - - await this._navigateUsingCRI(pageCriClient, url) - - if (options.experimentalSessionAndOrigin) { - await this._handlePausedRequests(pageCriClient) - _listenForFrameTreeChanges(pageCriClient) - } + await this.attachListeners(browser, url, pageCriClient, automation, options) // return the launched browser process // with additional method to close the remote connection diff --git a/packages/server/lib/browsers/cri-client.ts b/packages/server/lib/browsers/cri-client.ts index 44bcfdb3d626..2958c7946e8a 100644 --- a/packages/server/lib/browsers/cri-client.ts +++ b/packages/server/lib/browsers/cri-client.ts @@ -2,6 +2,7 @@ import debugModule from 'debug' import _ from 'lodash' import CRI from 'chrome-remote-interface' import * as errors from '../errors' +import type { CdpCommand, CdpEvent } from './cdp_automation' const debug = debugModule('cypress:server:browsers:cri-client') // debug using cypress-verbose:server:browsers:cri-client:send:* @@ -11,54 +12,25 @@ const debugVerboseReceive = debugModule('cypress-verbose:server:browsers:cri-cli const WEBSOCKET_NOT_OPEN_RE = /^WebSocket is (?:not open|already in CLOSING or CLOSED state)/ -/** - * Enumerations to make programming CDP slightly simpler - provides - * IntelliSense whenever you use named types. - */ -export namespace CRIWrapper { - export type Command = - 'Page.enable' | - 'Network.enable' | - 'Console.enable' | - 'Browser.getVersion' | - 'Page.bringToFront' | - 'Page.captureScreenshot' | - 'Page.navigate' | - 'Page.startScreencast' | - 'Page.screencastFrameAck' | - 'Page.setDownloadBehavior' | - string - - export type EventName = - 'Page.screencastFrame' | - 'Page.downloadWillBegin' | - 'Page.downloadProgress' | - string - +export interface CriClient { /** - * Wrapper for Chrome Remote Interface client. Only allows "send" method. - * @see https://github.com/cyrus-and/chrome-remote-interface#clientsendmethod-params-callback + * The target id attached to by this client */ - export interface Client { - /** - * The target id attached to by this client - */ - targetId: string - /** - * Sends a command to the Chrome remote interface. - * @example client.send('Page.navigate', { url }) - */ - send (command: Command, params?: object): Promise - /** - * Registers callback for particular event. - * @see https://github.com/cyrus-and/chrome-remote-interface#class-cdp - */ - on (eventName: EventName, cb: Function): void - /** - * Calls underlying remote interface client close - */ - close (): Promise - } + targetId: string + /** + * Sends a command to the Chrome remote interface. + * @example client.send('Page.navigate', { url }) + */ + send (command: CdpCommand, params?: object): Promise + /** + * Registers callback for particular event. + * @see https://github.com/cyrus-and/chrome-remote-interface#class-cdp + */ + on (eventName: CdpEvent, cb: Function): void + /** + * Calls underlying remote interface client close + */ + close (): Promise } const maybeDebugCdpMessages = (cri) => { @@ -104,16 +76,16 @@ const maybeDebugCdpMessages = (cri) => { type DeferredPromise = { resolve: Function, reject: Function } -export const create = async (target: string, onAsynchronousError: Function, host?: string, port?: number, onReconnect?: (client: CRIWrapper.Client) => void): Promise => { - const subscriptions: {eventName: CRIWrapper.EventName, cb: Function}[] = [] - const enableCommands: CRIWrapper.Command[] = [] - let enqueuedCommands: {command: CRIWrapper.Command, params: any, p: DeferredPromise }[] = [] +export const create = async (target: string, onAsynchronousError: Function, host?: string, port?: number, onReconnect?: (client: CriClient) => void): Promise => { + const subscriptions: {eventName: CdpEvent, cb: Function}[] = [] + const enableCommands: CdpCommand[] = [] + let enqueuedCommands: {command: CdpCommand, params: any, p: DeferredPromise }[] = [] let closed = false // has the user called .close on this? let connected = false // is this currently connected to CDP? let cri - let client: CRIWrapper.Client + let client: CriClient const reconnect = async () => { debug('disconnected, attempting to reconnect... %o', { closed }) @@ -184,7 +156,7 @@ export const create = async (target: string, onAsynchronousError: Function, host client = { targetId: target, - async send (command: CRIWrapper.Command, params?: object) { + async send (command: CdpCommand, params?: object) { const enqueue = () => { return new Promise((resolve, reject) => { enqueuedCommands.push({ command, params, p: { resolve, reject } }) diff --git a/packages/server/lib/browsers/electron.js b/packages/server/lib/browsers/electron.ts similarity index 61% rename from packages/server/lib/browsers/electron.js rename to packages/server/lib/browsers/electron.ts index 0f432da86184..5e022761a508 100644 --- a/packages/server/lib/browsers/electron.js +++ b/packages/server/lib/browsers/electron.ts @@ -1,15 +1,19 @@ -const _ = require('lodash') -const EE = require('events') -const path = require('path') -const Bluebird = require('bluebird') -const debug = require('debug')('cypress:server:browsers:electron') -const debugVerbose = require('debug')('cypress-verbose:server:browsers:electron') -const menu = require('../gui/menu') -const Windows = require('../gui/windows') -const { CdpAutomation, screencastOpts } = require('./cdp_automation') -const savedState = require('../saved_state') -const utils = require('./utils') -const errors = require('../errors') +import _ from 'lodash' +import EE from 'events' +import path from 'path' +import Debug from 'debug' +import menu from '../gui/menu' +import * as Windows from '../gui/windows' +import { CdpAutomation, screencastOpts, CdpCommand, CdpEvent } from './cdp_automation' +import * as savedState from '../saved_state' +import utils from './utils' +import * as errors from '../errors' +import type { BrowserInstance } from './types' +import type { BrowserWindow, WebContents } from 'electron' +import type { Automation } from '../automation' + +const debug = Debug('cypress:server:browsers:electron') +const debugVerbose = Debug('cypress-verbose:server:browsers:electron') // additional events that are nice to know about to be logged // https://electronjs.org/docs/api/browser-window#instance-events @@ -20,7 +24,7 @@ const ELECTRON_DEBUG_EVENTS = [ 'unresponsive', ] -let instance = null +let instance: BrowserInstance | null = null const tryToCall = function (win, method) { try { @@ -37,14 +41,14 @@ const tryToCall = function (win, method) { } const _getAutomation = async function (win, options, parent) { - const sendCommand = Bluebird.method((...args) => { + async function sendCommand (method: CdpCommand, data?: object) { return tryToCall(win, () => { return win.webContents.debugger.sendCommand - .apply(win.webContents.debugger, args) + .call(win.webContents.debugger, method, data) }) - }) + } - const on = (eventName, cb) => { + const on = (eventName: CdpEvent, cb) => { win.webContents.debugger.on('message', (event, method, params) => { if (method === eventName) { cb(params) @@ -89,16 +93,16 @@ const _getAutomation = async function (win, options, parent) { return automation } -const _installExtensions = function (win, extensionPaths = [], options) { +function _installExtensions (win: BrowserWindow, extensionPaths: string[], options) { Windows.removeAllExtensions(win) - return Bluebird.map(extensionPaths, (extensionPath) => { + return Promise.all(extensionPaths.map((extensionPath) => { try { return Windows.installExtension(win, extensionPath) } catch (error) { return options.onWarning(errors.get('EXTENSION_NOT_LOADED', 'Electron', extensionPath)) } - }) + })) } const _maybeRecordVideo = async function (webContents, options) { @@ -120,7 +124,7 @@ const _maybeRecordVideo = async function (webContents, options) { await webContents.debugger.sendCommand('Page.startScreencast', screencastOpts()) } -module.exports = { +export = { _defaultOptions (projectRoot, state, options, automation) { const _this = this @@ -149,24 +153,25 @@ module.exports = { return menu.set({ withInternalDevTools: true }) } }, - onNewWindow (e, url) { + async onNewWindow (this: BrowserWindow, e, url) { const _win = this - return _this._launchChild(e, url, _win, projectRoot, state, options, automation) - .then((child) => { - // close child on parent close - _win.on('close', () => { - if (!child.isDestroyed()) { - child.destroy() - } - }) - - // add this pid to list of pids - tryToCall(child, () => { - if (instance && instance.pid) { - instance.pid.push(child.webContents.getOSProcessId()) - } - }) + const child = await _this._launchChild(e, url, _win, projectRoot, state, options, automation) + + // close child on parent close + _win.on('close', () => { + if (!child.isDestroyed()) { + child.destroy() + } + }) + + // add this pid to list of pids + tryToCall(child, () => { + if (instance && instance.pid) { + if (!instance.allPids) throw new Error('Missing allPids!') + + instance.allPids.push(child.webContents.getOSProcessId()) + } }) }, } @@ -182,7 +187,7 @@ module.exports = { _getAutomation, - async _render (url, automation, preferences = {}, options = {}) { + async _render (url: string, automation: Automation, preferences, options: { projectRoot: string, isTextTerminal: boolean }) { const win = Windows.create(options.projectRoot, preferences) if (preferences.browser.isHeadless) { @@ -195,9 +200,11 @@ module.exports = { win.maximize() } - return this._launch(win, url, automation, preferences).tap(async () => { - automation.use(await _getAutomation(win, preferences, automation)) - }) + const launched = await this._launch(win, url, automation, preferences) + + automation.use(await _getAutomation(win, preferences, automation)) + + return launched }, _launchChild (e, url, parent, projectRoot, state, options, automation) { @@ -205,7 +212,7 @@ module.exports = { const [parentX, parentY] = parent.getPosition() - options = this._defaultOptions(projectRoot, state, options) + options = this._defaultOptions(projectRoot, state, options, automation) _.extend(options, { x: parentX + 100, @@ -222,75 +229,68 @@ module.exports = { return this._launch(win, url, automation, options) }, - _launch (win, url, automation, options) { + async _launch (win: BrowserWindow, url: string, automation: Automation, options) { if (options.show) { menu.set({ withInternalDevTools: true }) } ELECTRON_DEBUG_EVENTS.forEach((e) => { + // @ts-expect-error mapping strings to event names is failing typecheck win.on(e, () => { debug('%s fired on the BrowserWindow %o', e, { browserWindowUrl: url }) }) }) - return Bluebird.try(() => { - return this._attachDebugger(win.webContents) - }) - .then(() => { - let ua + this._attachDebugger(win.webContents) - ua = options.userAgent + let ua - if (ua) { - this._setUserAgent(win.webContents, ua) - // @see https://github.com/cypress-io/cypress/issues/22953 - } else if (options.experimentalModifyObstructiveThirdPartyCode) { - const userAgent = this._getUserAgent(win.webContents) - // replace any obstructive electron user agents that contain electron or cypress references to appear more chrome-like - const modifiedNonObstructiveUserAgent = userAgent.replace(/Cypress.*?\s|[Ee]lectron.*?\s/g, '') + ua = options.userAgent - this._setUserAgent(win.webContents, modifiedNonObstructiveUserAgent) - } + if (ua) { + this._setUserAgent(win.webContents, ua) + // @see https://github.com/cypress-io/cypress/issues/22953 + } else if (options.experimentalModifyObstructiveThirdPartyCode) { + const userAgent = this._getUserAgent(win.webContents) + // replace any obstructive electron user agents that contain electron or cypress references to appear more chrome-like + const modifiedNonObstructiveUserAgent = userAgent.replace(/Cypress.*?\s|[Ee]lectron.*?\s/g, '') - const setProxy = () => { - let ps + this._setUserAgent(win.webContents, modifiedNonObstructiveUserAgent) + } - ps = options.proxyServer + const setProxy = () => { + let ps - if (ps) { - return this._setProxy(win.webContents, ps) - } - } + ps = options.proxyServer - return Bluebird.join( - setProxy(), - this._clearCache(win.webContents), - ) - }) - .then(() => { - return win.loadURL('about:blank') - }) - .then(() => this._getAutomation(win, options, automation)) - .then((cdpAutomation) => automation.use(cdpAutomation)) - .then(() => { - return Promise.all([ - _maybeRecordVideo(win.webContents, options), - this._handleDownloads(win, options.downloadsFolder, automation), - ]) - }) - .then(() => { - // enabling can only happen once the window has loaded - return this._enableDebugger(win.webContents) - }) - .then(() => { - return win.loadURL(url) - }) - .then(() => { - if (options.experimentalSessionAndOrigin) { - this._listenToOnBeforeHeaders(win) + if (ps) { + return this._setProxy(win.webContents, ps) } - }) - .return(win) + } + + await Promise.all([ + setProxy(), + this._clearCache(win.webContents), + ]) + + await win.loadURL('about:blank') + const cdpAutomation = await this._getAutomation(win, options, automation) + + automation.use(cdpAutomation) + await Promise.all([ + _maybeRecordVideo(win.webContents, options), + this._handleDownloads(win, options.downloadsFolder, automation), + ]) + + // enabling can only happen once the window has loaded + await this._enableDebugger(win.webContents) + + await win.loadURL(url) + if (options.experimentalSessionAndOrigin) { + this._listenToOnBeforeHeaders(win) + } + + return win }, _attachDebugger (webContents) { @@ -304,11 +304,12 @@ module.exports = { const originalSendCommand = webContents.debugger.sendCommand - webContents.debugger.sendCommand = function (message, data) { + webContents.debugger.sendCommand = async function (message, data) { debugVerbose('debugger: sending %s with params %o', message, data) - return originalSendCommand.call(webContents.debugger, message, data) - .then((res) => { + try { + const res = await originalSendCommand.call(webContents.debugger, message, data) + let debugRes = res if (debug.enabled && (_.get(debugRes, 'data.length') > 100)) { @@ -319,10 +320,10 @@ module.exports = { debugVerbose('debugger: received response to %s: %o', message, debugRes) return res - }).catch((err) => { + } catch (err) { debug('debugger: received error on %s: %o', message, err) throw err - }) + } } webContents.debugger.sendCommand('Browser.getVersion') @@ -338,7 +339,7 @@ module.exports = { }) }, - _enableDebugger (webContents) { + _enableDebugger (webContents: WebContents) { debug('debugger: enable Console and Network') return webContents.debugger.sendCommand('Console.enable') @@ -375,7 +376,7 @@ module.exports = { }) }, - _listenToOnBeforeHeaders (win) { + _listenToOnBeforeHeaders (win: BrowserWindow) { // true if the frame only has a single parent, false otherwise const isFirstLevelIFrame = (frame) => (!!frame?.parent && !frame.parent.parent) @@ -449,85 +450,87 @@ module.exports = { }, async connectToNewSpec (browser, options, automation) { - this.open(browser, options.url, options, automation) + if (!options.url) throw new Error('Missing url in connectToNewSpec') + + await this.open(browser, options.url, options, automation) }, - async connectToExisting () { + connectToExisting () { throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron') }, - open (browser, url, options = {}, automation) { + async open (browser, url, options, automation) { const { projectRoot, isTextTerminal } = options debug('open %o', { browser, url }) - return savedState.create(projectRoot, isTextTerminal) - .then((state) => { - return state.get() - }).then((state) => { - debug('received saved state %o', state) + const State = await savedState.create(projectRoot, isTextTerminal) + const state = await State.get() - // get our electron default options - // TODO: this is bad, don't mutate the options object - options = this._defaultOptions(projectRoot, state, options, automation) + debug('received saved state %o', state) - // get the GUI window defaults now - options = Windows.defaults(options) + // get our electron default options + // TODO: this is bad, don't mutate the options object + options = this._defaultOptions(projectRoot, state, options, automation) - debug('browser window options %o', _.omitBy(options, _.isFunction)) + // get the GUI window defaults now + options = Windows.defaults(options) - const defaultLaunchOptions = utils.getDefaultLaunchOptions({ - preferences: options, - }) + debug('browser window options %o', _.omitBy(options, _.isFunction)) - return utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options) - }).then((launchOptions) => { - const { preferences } = launchOptions + const defaultLaunchOptions = utils.getDefaultLaunchOptions({ + preferences: options, + }) - debug('launching browser window to url: %s', url) + const launchOptions = await utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options) - return this._render(url, automation, preferences, { - projectRoot: options.projectRoot, - isTextTerminal: options.isTextTerminal, - }) - .then(async (win) => { - await _installExtensions(win, launchOptions.extensions, options) + const { preferences } = launchOptions + + debug('launching browser window to url: %s', url) - // cause the webview to receive focus so that - // native browser focus + blur events fire correctly - // https://github.com/cypress-io/cypress/issues/1939 - tryToCall(win, 'focusOnWebView') + const win = await this._render(url, automation, preferences, { + projectRoot: options.projectRoot, + isTextTerminal: options.isTextTerminal, + }) - const events = new EE + await _installExtensions(win, launchOptions.extensions, options) - win.once('closed', () => { - debug('closed event fired') + // cause the webview to receive focus so that + // native browser focus + blur events fire correctly + // https://github.com/cypress-io/cypress/issues/1939 + tryToCall(win, 'focusOnWebView') - Windows.removeAllExtensions(win) + const events = new EE() - return events.emit('exit') - }) + win.once('closed', () => { + debug('closed event fired') - instance = _.extend(events, { - pid: [tryToCall(win, () => { - return win.webContents.getOSProcessId() - })], - browserWindow: win, - kill () { - if (this.isProcessExit) { - // if the process is exiting, all BrowserWindows will be destroyed anyways - return - } - - return tryToCall(win, 'destroy') - }, - removeAllListeners () { - return tryToCall(win, 'removeAllListeners') - }, - }) + Windows.removeAllExtensions(win) - return instance - }) + return events.emit('exit') + }) + + const mainPid: number = tryToCall(win, () => { + return win.webContents.getOSProcessId() }) + + instance = _.extend(events, { + pid: mainPid, + allPids: [mainPid], + browserWindow: win, + kill (this: BrowserInstance) { + if (this.isProcessExit) { + // if the process is exiting, all BrowserWindows will be destroyed anyways + return + } + + return tryToCall(win, 'destroy') + }, + removeAllListeners () { + return tryToCall(win, 'removeAllListeners') + }, + }) as BrowserInstance + + return instance }, } diff --git a/packages/server/lib/browsers/index.js b/packages/server/lib/browsers/index.js deleted file mode 100644 index 69df090f3c66..000000000000 --- a/packages/server/lib/browsers/index.js +++ /dev/null @@ -1,217 +0,0 @@ -const _ = require('lodash') -const Promise = require('bluebird') -const debug = require('debug')('cypress:server:browsers') -const utils = require('./utils') -const check = require('check-more-types') -const { exec } = require('child_process') -const util = require('util') -const os = require('os') -const { BROWSER_FAMILY } = require('@packages/types') - -const isBrowserFamily = check.oneOf(BROWSER_FAMILY) - -let instance = null - -const kill = function (unbind = true, isProcessExit = false) { - // Clean up the instance when the browser is closed - if (!instance) { - debug('browsers.kill called with no active instance') - - return Promise.resolve() - } - - const _instance = instance - - instance = null - - return new Promise((resolve) => { - _instance.once('exit', () => { - if (unbind) { - _instance.removeAllListeners() - } - - debug('browser process killed') - - resolve() - }) - - debug('killing browser process') - - _instance.isProcessExit = isProcessExit - - _instance.kill() - }) -} - -const setFocus = async function () { - const platform = os.platform() - const execAsync = util.promisify(exec) - - try { - switch (platform) { - case 'darwin': - return execAsync(`open -a "$(ps -p ${instance.pid} -o comm=)"`) - case 'win32': { - return execAsync(`(New-Object -ComObject WScript.Shell).AppActivate(((Get-WmiObject -Class win32_process -Filter "ParentProcessID = '${instance.pid}'") | Select -ExpandProperty ProcessId))`, { shell: 'powershell.exe' }) - } - default: - debug(`Unexpected os platform ${platform}. Set focus is only functional on Windows and MacOS`) - } - } catch (error) { - debug(`Failure to set focus. ${error}`) - } -} - -const getBrowserLauncher = function (browser) { - debug('getBrowserLauncher %o', { browser }) - if (!isBrowserFamily(browser.family)) { - debug('unknown browser family', browser.family) - } - - if (browser.name === 'electron') { - return require('./electron') - } - - if (browser.family === 'chromium') { - return require('./chrome') - } - - if (browser.family === 'firefox') { - return require('./firefox') - } - - if (browser.family === 'webkit') { - return require('./webkit') - } -} - -process.once('exit', () => kill(true, true)) - -module.exports = { - ensureAndGetByNameOrPath: utils.ensureAndGetByNameOrPath, - - isBrowserFamily, - - removeOldProfiles: utils.removeOldProfiles, - - get: utils.getBrowsers, - - close: kill, - - formatBrowsersToOptions: utils.formatBrowsersToOptions, - - _setInstance (_instance) { - // for testing - instance = _instance - }, - - // note: does not guarantee that `browser` is still running - // note: electron will return a list of pids for each webContent - getBrowserInstance () { - return instance - }, - - getAllBrowsersWith (nameOrPath) { - debug('getAllBrowsersWith %o', { nameOrPath }) - if (nameOrPath) { - return utils.ensureAndGetByNameOrPath(nameOrPath, true) - } - - return utils.getBrowsers() - }, - - async connectToExisting (browser, options = {}, automation) { - const browserLauncher = getBrowserLauncher(browser) - - if (!browserLauncher) { - utils.throwBrowserNotFound(browser.name, options.browsers) - } - - await browserLauncher.connectToExisting(browser, options, automation) - - return this.getBrowserInstance() - }, - - async connectToNewSpec (browser, options = {}, automation) { - const browserLauncher = getBrowserLauncher(browser) - - if (!browserLauncher) { - utils.throwBrowserNotFound(browser.name, options.browsers) - } - - // Instance will be null when we're dealing with electron. In that case we don't need a browserCriClient - await browserLauncher.connectToNewSpec(browser, options, automation) - - return this.getBrowserInstance() - }, - - open (browser, options = {}, automation, ctx) { - return kill(true) - .then(() => { - _.defaults(options, { - onBrowserOpen () {}, - onBrowserClose () {}, - }) - - ctx.browser.setBrowserStatus('opening') - - const browserLauncher = getBrowserLauncher(browser) - - if (!browserLauncher) { - utils.throwBrowserNotFound(browser.name, options.browsers) - } - - const { url } = options - - if (!url) { - throw new Error('options.url must be provided when opening a browser. You passed:', options) - } - - debug('opening browser %o', browser) - - return browserLauncher.open(browser, url, options, automation) - .then((i) => { - debug('browser opened') - // TODO: bind to process.exit here - // or move this functionality into cypress-core-launder - - i.browser = browser - - instance = i - - // TODO: normalizing opening and closing / exiting - // so that there is a default for each browser but - // enable the browser to configure the interface - instance.once('exit', () => { - ctx.browser.setBrowserStatus('closed') - options.onBrowserClose() - instance = null - }) - - // TODO: instead of waiting an arbitrary - // amount of time here we could instead - // wait for the socket.io connect event - // which would mean that our browser is - // completely rendered and open. that would - // mean moving this code out of here and - // into the project itself - // (just like headless code) - // ---------------------------- - // give a little padding around - // the browser opening - return Promise.delay(1000) - .then(() => { - if (instance === null) { - return null - } - - options.onBrowserOpen() - ctx.browser.setBrowserStatus('open') - - return instance - }) - }) - }) - }, - setFocus, -} diff --git a/packages/server/lib/browsers/index.ts b/packages/server/lib/browsers/index.ts new file mode 100644 index 000000000000..89dd0dc087c5 --- /dev/null +++ b/packages/server/lib/browsers/index.ts @@ -0,0 +1,200 @@ +import _ from 'lodash' +import Bluebird from 'bluebird' +import Debug from 'debug' +import utils from './utils' +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 type { Browser, BrowserInstance, BrowserLauncher } from './types' +import type { Automation } from '../automation' + +const debug = Debug('cypress:server:browsers') +const isBrowserFamily = check.oneOf(BROWSER_FAMILY) + +let instance: BrowserInstance | null = null + +const kill = function (unbind = true, isProcessExit = false) { + // Clean up the instance when the browser is closed + if (!instance) { + debug('browsers.kill called with no active instance') + + return Promise.resolve() + } + + const _instance = instance + + instance = null + + return new Promise((resolve) => { + _instance.once('exit', () => { + if (unbind) { + _instance.removeAllListeners() + } + + debug('browser process killed') + + resolve() + }) + + debug('killing browser process') + + _instance.isProcessExit = isProcessExit + + _instance.kill() + }) +} + +async function setFocus () { + const platform = os.platform() + const execAsync = util.promisify(exec) + + try { + if (!instance) throw new Error('No instance in setFocus!') + + switch (platform) { + case 'darwin': + await execAsync(`open -a "$(ps -p ${instance.pid} -o comm=)"`) + + return + case 'win32': { + await execAsync(`(New-Object -ComObject WScript.Shell).AppActivate(((Get-WmiObject -Class win32_process -Filter "ParentProcessID = '${instance.pid}'") | Select -ExpandProperty ProcessId))`, { shell: 'powershell.exe' }) + + return + } + default: + debug(`Unexpected os platform ${platform}. Set focus is only functional on Windows and MacOS`) + } + } catch (error) { + debug(`Failure to set focus. ${error}`) + } +} + +async function getBrowserLauncher (browser: Browser, browsers: FoundBrowser[]): Promise { + debug('getBrowserLauncher %o', { browser }) + + if (browser.name === 'electron') return await import('./electron') + + if (browser.family === 'chromium') return await import('./chrome') + + if (browser.family === 'firefox') return await import('./firefox') + + if (browser.family === 'webkit') return await import('./webkit') + + return utils.throwBrowserNotFound(browser.name, browsers) +} + +process.once('exit', () => kill(true, true)) + +export = { + ensureAndGetByNameOrPath: utils.ensureAndGetByNameOrPath, + + isBrowserFamily, + + removeOldProfiles: utils.removeOldProfiles, + + get: utils.getBrowsers, + + close: kill, + + formatBrowsersToOptions: utils.formatBrowsersToOptions, + + _setInstance (_instance: BrowserInstance) { + // for testing + instance = _instance + }, + + // note: does not guarantee that `browser` is still running + getBrowserInstance () { + return instance + }, + + getAllBrowsersWith (nameOrPath?: string) { + debug('getAllBrowsersWith %o', { nameOrPath }) + if (nameOrPath) { + return utils.ensureAndGetByNameOrPath(nameOrPath, true) + } + + return utils.getBrowsers() + }, + + async connectToExisting (browser: Browser, options: BrowserLaunchOpts, automation: Automation): Promise { + const browserLauncher = await getBrowserLauncher(browser, options.browsers) + + await browserLauncher.connectToExisting(browser, options, automation) + + return this.getBrowserInstance() + }, + + async connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation): Promise { + const browserLauncher = await getBrowserLauncher(browser, options.browsers) + + // Instance will be null when we're dealing with electron. In that case we don't need a browserCriClient + await browserLauncher.connectToNewSpec(browser, options, automation) + + return this.getBrowserInstance() + }, + + async open (browser: Browser, options: BrowserLaunchOpts, automation: Automation, ctx): Promise { + await kill(true) + + _.defaults(options, { + onBrowserOpen () {}, + onBrowserClose () {}, + }) + + ctx.browser.setBrowserStatus('opening') + + const browserLauncher = await getBrowserLauncher(browser, options.browsers) + + if (!options.url) throw new Error('Missing url in browsers.open') + + debug('opening browser %o', browser) + + const _instance = await browserLauncher.open(browser, options.url, options, automation) + + debug('browser opened') + + instance = _instance + instance.browser = browser + + // TODO: normalizing opening and closing / exiting + // so that there is a default for each browser but + // enable the browser to configure the interface + instance.once('exit', () => { + ctx.browser.setBrowserStatus('closed') + // TODO: make this a required property + if (!options.onBrowserClose) throw new Error('onBrowserClose did not exist in interactive mode') + + options.onBrowserClose() + instance = null + }) + + // TODO: instead of waiting an arbitrary + // amount of time here we could instead + // wait for the socket.io connect event + // which would mean that our browser is + // completely rendered and open. that would + // mean moving this code out of here and + // into the project itself + // (just like headless code) + // ---------------------------- + // give a little padding around + // the browser opening + await Bluebird.delay(1000) + + if (instance === null) { + return null + } + + // TODO: make this a required property + if (!options.onBrowserOpen) throw new Error('onBrowserOpen did not exist in interactive mode') + + options.onBrowserOpen() + ctx.browser.setBrowserStatus('open') + + return instance + }, + setFocus, +} as const diff --git a/packages/server/lib/browsers/types.ts b/packages/server/lib/browsers/types.ts index 8259cc6dee68..08dafccf42a3 100644 --- a/packages/server/lib/browsers/types.ts +++ b/packages/server/lib/browsers/types.ts @@ -1,5 +1,6 @@ -import type { FoundBrowser } from '@packages/types' +import type { FoundBrowser, BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types' import type { EventEmitter } from 'events' +import type { Automation } from '../automation' export type Browser = FoundBrowser & { majorVersion: number @@ -9,5 +10,29 @@ export type Browser = FoundBrowser & { export type BrowserInstance = EventEmitter & { kill: () => void + /** + * Used in Electron to keep a list of what pids are spawned by the browser, to keep them separate from the launchpad/server pids. + * In all other browsers, the process tree of `BrowserInstance.pid` can be used instead of `allPids`. + */ + allPids?: number[] pid: number + /** + * After `.open`, this is set to the `Browser` used to launch this instance. + * TODO: remove need for this + */ + browser?: Browser + /** + * If set, the browser is currently in the process of exiting due to the parent process exiting. + * TODO: remove need for this + */ + isProcessExit?: boolean +} + +export type BrowserLauncher = { + open: (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation) => Promise + connectToNewSpec: (browser: Browser, options: BrowserNewTabOpts, automation: Automation) => Promise + /** + * Used in Cypress-in-Cypress tests to connect to the existing browser instance. + */ + connectToExisting: (browser: Browser, options: BrowserLaunchOpts, automation: Automation) => void | Promise } diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 3963d8d7fb4e..7c9062534d83 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -293,8 +293,8 @@ const parseBrowserOption = (opt) => { } } -function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers: FoundBrowser[]): Bluebird -function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers: FoundBrowser[]): Bluebird +function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers?: FoundBrowser[]): Bluebird +function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers?: FoundBrowser[]): Bluebird async function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, prevKnownBrowsers: FoundBrowser[] = []) { const browsers = prevKnownBrowsers.length ? prevKnownBrowsers : (await getBrowsers()) diff --git a/packages/server/lib/browsers/webkit.ts b/packages/server/lib/browsers/webkit.ts index 58c7661856c6..0eb3399a64b1 100644 --- a/packages/server/lib/browsers/webkit.ts +++ b/packages/server/lib/browsers/webkit.ts @@ -4,12 +4,13 @@ import type playwright from 'playwright-webkit' import type { Browser, BrowserInstance } from './types' import type { Automation } from '../automation' import { WebKitAutomation } from './webkit-automation' +import type { BrowserLaunchOpts, BrowserNewTabOpts } from '@packages/types' const debug = Debug('cypress:server:browsers:webkit') let wkAutomation: WebKitAutomation | undefined -export async function connectToNewSpec (browser: Browser, options, automation: Automation) { +export async function connectToNewSpec (browser: Browser, options: BrowserNewTabOpts, automation: Automation) { if (!wkAutomation) throw new Error('connectToNewSpec called without wkAutomation') automation.use(wkAutomation) @@ -18,7 +19,11 @@ export async function connectToNewSpec (browser: Browser, options, automation: A await wkAutomation.reset(options.url) } -export async function open (browser: Browser, url, options: any = {}, automation: Automation): Promise { +export function connectToExisting () { + throw new Error('Cypress-in-Cypress is not supported for WebKit.') +} + +export async function open (browser: Browser, url: string, options: BrowserLaunchOpts, automation: Automation): Promise { // resolve pw from user's project path const pwModulePath = require.resolve('playwright-webkit', { paths: [process.cwd()] }) const pw = require(pwModulePath) as typeof playwright diff --git a/packages/server/lib/makeDataContext.ts b/packages/server/lib/makeDataContext.ts index 2fcc8be88fb1..eb2c4cb59688 100644 --- a/packages/server/lib/makeDataContext.ts +++ b/packages/server/lib/makeDataContext.ts @@ -7,9 +7,9 @@ import { isMainWindowFocused, focusMainWindow } from './gui/windows' import type { AllModeOptions, AllowedState, + OpenProjectLaunchOpts, FoundBrowser, InitializeProjectOptions, - LaunchOpts, OpenProjectLaunchOptions, Preferences, } from '@packages/types' @@ -75,7 +75,7 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext { }, }, projectApi: { - launchProject (browser: FoundBrowser, spec: Cypress.Spec, options?: LaunchOpts) { + launchProject (browser: FoundBrowser, spec: Cypress.Spec, options: OpenProjectLaunchOpts) { return openProject.launch({ ...browser }, spec, options) }, openProjectCreate (args: InitializeProjectOptions, options: OpenProjectLaunchOptions) { diff --git a/packages/server/lib/modes/run.ts b/packages/server/lib/modes/run.ts index 48dec59c4497..0343e4427938 100644 --- a/packages/server/lib/modes/run.ts +++ b/packages/server/lib/modes/run.ts @@ -22,7 +22,7 @@ import random from '../util/random' import system from '../util/system' import chromePolicyCheck from '../util/chrome_policy_check' import * as objUtils from '../util/obj_utils' -import type { SpecWithRelativeRoot, LaunchOpts, SpecFile, TestingType } from '@packages/types' +import type { SpecWithRelativeRoot, SpecFile, TestingType, OpenProjectLaunchOpts, FoundBrowser } from '@packages/types' import type { Cfg } from '../project-base' import type { Browser } from '../browsers/types' import * as printResults from '../util/print-run' @@ -129,7 +129,7 @@ const getDefaultBrowserOptsByFamily = (browser, project, writeVideoFrame, onErro } if (browser.family === 'chromium') { - return getChromeProps(writeVideoFrame) + return getCdpVideoProp(writeVideoFrame) } if (browser.family === 'firefox') { @@ -149,33 +149,22 @@ const getFirefoxProps = (project, writeVideoFrame) => { return {} } -const getCdpVideoPropSetter = (writeVideoFrame) => { +const getCdpVideoProp = (writeVideoFrame) => { if (!writeVideoFrame) { - return _.noop + return {} } - return (props) => { - props.onScreencastFrame = (e) => { + return { + onScreencastFrame: (e) => { // https://chromedevtools.github.io/devtools-protocol/tot/Page#event-screencastFrame writeVideoFrame(Buffer.from(e.data, 'base64')) - } + }, } } -const getChromeProps = (writeVideoFrame) => { - const shouldWriteVideo = Boolean(writeVideoFrame) - - debug('setting Chrome properties %o', { shouldWriteVideo }) - - return _ - .chain({}) - .tap(getCdpVideoPropSetter(writeVideoFrame)) - .value() -} - const getElectronProps = (isHeaded, writeVideoFrame, onError) => { - return _ - .chain({ + return { + ...getCdpVideoProp(writeVideoFrame), width: 1280, height: 720, show: isHeaded, @@ -193,9 +182,7 @@ const getElectronProps = (isHeaded, writeVideoFrame, onError) => { // https://github.com/cypress-io/cypress/issues/123 options.show = false }, - }) - .tap(getCdpVideoPropSetter(writeVideoFrame)) - .value() + } } const sumByProp = (runs, prop) => { @@ -331,13 +318,11 @@ async function maybeStartVideoRecording (options: { spec: SpecWithRelativeRoot, const { spec, browser, video, videosFolder } = options debug(`video recording has been ${video ? 'enabled' : 'disabled'}. video: %s`, video) - // bail if we've been told not to capture - // a video recording + if (!video) { return } - // make sure we have a videosFolder if (!videosFolder) { throw new Error('Missing videoFolder for recording') } @@ -400,7 +385,7 @@ function launchBrowser (options: { browser: Browser, spec: SpecWithRelativeRoot, const warnings = {} - const browserOpts: LaunchOpts = { + const browserOpts: OpenProjectLaunchOpts = { ...getDefaultBrowserOptsByFamily(browser, project, writeVideoFrame, onError), projectRoot, shouldLaunchNewTab, @@ -548,11 +533,11 @@ function waitForBrowserToConnect (options: { project: Project, socketId: string, const wait = () => { debug('waiting for socket to connect and browser to launch...') - return Bluebird.join( + return Bluebird.all([ waitForSocketConnection(project, socketId), // TODO: remove the need to extend options and coerce this type launchBrowser(options as typeof options & { setScreenshotMetadata: SetScreenshotMetadata }), - ) + ]) .timeout(browserTimeout) .catch(Bluebird.TimeoutError, async (err) => { attempts += 1 @@ -943,7 +928,7 @@ async function runSpec (config, spec: SpecWithRelativeRoot, options: { project: return { results } } -async function ready (options: { projectRoot: string, record: boolean, key: string, ciBuildId: string, parallel: boolean, group: string, browser: string, tag: string, testingType: TestingType, socketId: string, spec: string | RegExp | string[], headed: boolean, outputPath: string, exit: boolean, quiet: boolean, onError?: (err: Error) => void, browsers?: Browser[], webSecurity: boolean }) { +async function ready (options: { projectRoot: string, record: boolean, key: string, ciBuildId: string, parallel: boolean, group: string, browser: string, tag: string, testingType: TestingType, socketId: string, spec: string | RegExp | string[], headed: boolean, outputPath: string, exit: boolean, quiet: boolean, onError?: (err: Error) => void, browsers?: FoundBrowser[], webSecurity: boolean }) { debug('run mode ready with options %o', options) if (process.env.ELECTRON_RUN_AS_NODE && !process.env.DISPLAY) { @@ -1001,11 +986,11 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri const [sys, browser] = await Promise.all([ system.info(), (async () => { - const browsers = await browserUtils.ensureAndGetByNameOrPath(browserName, false, userBrowsers) + const browser = await browserUtils.ensureAndGetByNameOrPath(browserName, false, userBrowsers) - await removeOldProfiles(browsers) + await removeOldProfiles(browser) - return browsers + return browser })(), trashAssets(config), ]) @@ -1033,6 +1018,8 @@ async function ready (options: { projectRoot: string, record: boolean, key: stri socketId, parallel, onError, + // TODO: refactor this so that augmenting the browser object here is not needed and there is no type conflict + // @ts-expect-error runSpecs augments browser with isHeadless and isHeaded, which is "missing" from the type here browser, project, runUrl, diff --git a/packages/server/lib/open_project.ts b/packages/server/lib/open_project.ts index 6c5fce26d398..632e0dc14c63 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -12,7 +12,7 @@ import runEvents from './plugins/run_events' import * as session from './session' import { cookieJar } from './util/cookies' import { getSpecUrl } from './project_utils' -import type { LaunchOpts, OpenProjectLaunchOptions, InitializeProjectOptions } from '@packages/types' +import type { BrowserLaunchOpts, OpenProjectLaunchOptions, InitializeProjectOptions, OpenProjectLaunchOpts, FoundBrowser } from '@packages/types' import { DataContext, getCtx } from '@packages/data-context' import { autoBindDebug } from '@packages/data-context/src/util' @@ -20,7 +20,7 @@ const debug = Debug('cypress:server:open_project') export class OpenProject { private projectBase: ProjectBase | null = null - relaunchBrowser: ((...args: unknown[]) => Bluebird) | null = null + relaunchBrowser: (() => Promise) | null = null constructor () { return autoBindDebug(this) @@ -48,15 +48,13 @@ export class OpenProject { return this.projectBase } - async launch (browser, spec: Cypress.Cypress['spec'], options: LaunchOpts = { - onError: () => undefined, - }) { + async launch (browser, spec: Cypress.Cypress['spec'], prevOptions?: OpenProjectLaunchOpts) { this._ctx = getCtx() assert(this.projectBase, 'Cannot launch runner if projectBase is undefined!') debug('resetting project state, preparing to launch browser %s for spec %o options %o', - browser.name, spec, options) + browser.name, spec, prevOptions) la(_.isPlainObject(browser), 'expected browser object:', browser) @@ -64,7 +62,7 @@ export class OpenProject { // of potential domain changes, request buffers, etc this.projectBase!.reset() - let url = getSpecUrl({ + const url = process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF ? undefined : getSpecUrl({ spec, browserUrl: this.projectBase.cfg.browserUrl, projectRoot: this.projectBase.projectRoot, @@ -74,8 +72,13 @@ export class OpenProject { const cfg = this.projectBase.getConfig() - _.defaults(options, { - browsers: cfg.browsers, + if (!cfg.proxyServer) throw new Error('Missing proxyServer in launch') + + const options: BrowserLaunchOpts = { + browser, + url, + // TODO: fix majorVersion discrepancy that causes this to be necessary + browsers: cfg.browsers as FoundBrowser[], userAgent: cfg.userAgent, proxyUrl: cfg.proxyUrl, proxyServer: cfg.proxyServer, @@ -85,7 +88,8 @@ export class OpenProject { downloadsFolder: cfg.downloadsFolder, experimentalSessionAndOrigin: cfg.experimentalSessionAndOrigin, experimentalModifyObstructiveThirdPartyCode: cfg.experimentalModifyObstructiveThirdPartyCode, - }) + ...prevOptions || {}, + } // if we don't have the isHeaded property // then we're in interactive mode and we @@ -96,21 +100,13 @@ export class OpenProject { browser.isHeadless = false } - // set the current browser object on options - // so we can pass it down - options.browser = browser - - if (!process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) { - options.url = url - } - this.projectBase.setCurrentSpecAndBrowser(spec, browser) const automation = this.projectBase.getAutomation() // use automation middleware if its // been defined here - let am = options.automationMiddleware + const am = options.automationMiddleware if (am) { automation.use(am) @@ -155,41 +151,38 @@ export class OpenProject { options.onError = this.projectBase.options.onError - this.relaunchBrowser = () => { + this.relaunchBrowser = async () => { debug( 'launching browser: %o, spec: %s', browser, spec.relative, ) - return Bluebird.try(() => { - if (!cfg.isTextTerminal && cfg.experimentalInteractiveRunEvents) { - return runEvents.execute('before:spec', cfg, spec) - } - + if (!cfg.isTextTerminal && cfg.experimentalInteractiveRunEvents) { + await runEvents.execute('before:spec', cfg, spec) + } else { // clear cookies and all session data before each spec cookieJar.removeAllCookies() session.clearSessions() - }) - .then(() => { - // TODO: Stub this so we can detect it being called - if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) { - return browsers.connectToExisting(browser, options, automation) - } + } - if (options.shouldLaunchNewTab) { - const onInitializeNewBrowserTab = async () => { - await this.resetBrowserState() - } + // TODO: Stub this so we can detect it being called + if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) { + return await browsers.connectToExisting(browser, options, automation) + } - // If we do not launch the browser, - // we tell it that we are ready - // to receive the next spec - return browsers.connectToNewSpec(browser, { onInitializeNewBrowserTab, ...options }, automation) + if (options.shouldLaunchNewTab) { + const onInitializeNewBrowserTab = async () => { + await this.resetBrowserState() } - return browsers.open(browser, options, automation, this._ctx) - }) + // If we do not launch the browser, + // we tell it that we are ready + // to receive the next spec + return await browsers.connectToNewSpec(browser, { onInitializeNewBrowserTab, ...options }, automation) + } + + return await browsers.open(browser, options, automation, this._ctx) } return this.relaunchBrowser() @@ -220,7 +213,7 @@ export class OpenProject { close () { debug('closing opened project') - this.closeOpenProjectAndBrowsers() + return this.closeOpenProjectAndBrowsers() } changeUrlToSpec (spec: Cypress.Spec) { diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index bd34d99478fb..bdc4e0bb55c6 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -296,14 +296,6 @@ export class ProjectBase extends EE { return runEvents.execute('after:run', config) } - _onError> (err: Error, options: Options) { - debug('got plugins error', err.stack) - - browsers.close() - - options.onError(err) - } - initializeReporter ({ report, reporter, diff --git a/packages/server/lib/util/process_profiler.ts b/packages/server/lib/util/process_profiler.ts index 01563aeb28e5..d228c7b30bd9 100644 --- a/packages/server/lib/util/process_profiler.ts +++ b/packages/server/lib/util/process_profiler.ts @@ -51,9 +51,9 @@ export const _groupCyProcesses = ({ list }: si.Systeminformation.ProcessesData) const isBrowserProcess = (proc: Process): boolean => { const instance = browsers.getBrowserInstance() // electron will return a list of pids, since it's not a hierarchy - const pid: number | number[] = instance && instance.pid + const pids: number[] = instance.allPids ? instance.allPids : [instance.pid] - return (Array.isArray(pid) ? (pid as number[]).includes(proc.pid) : proc.pid === pid) + return (pids.includes(proc.pid)) || isParentProcessInGroup(proc, 'browser') } diff --git a/packages/server/test/unit/browsers/chrome_spec.js b/packages/server/test/unit/browsers/chrome_spec.js index 52c9ad294d57..75352873edde 100644 --- a/packages/server/test/unit/browsers/chrome_spec.js +++ b/packages/server/test/unit/browsers/chrome_spec.js @@ -13,6 +13,10 @@ const chrome = require(`../../../lib/browsers/chrome`) const { fs } = require(`../../../lib/util/fs`) const { BrowserCriClient } = require('../../../lib/browsers/browser-cri-client') +const openOpts = { + onError: () => {}, +} + describe('lib/browsers/chrome', () => { context('#open', () => { beforeEach(function () { @@ -45,7 +49,7 @@ describe('lib/browsers/chrome', () => { this.onCriEvent = (event, data, options) => { this.pageCriClient.on.withArgs(event).yieldsAsync(data) - return chrome.open({ isHeadless: true }, 'http://', options, this.automation) + return chrome.open({ isHeadless: true }, 'http://', { ...openOpts, ...options }, this.automation) .then(() => { this.pageCriClient.on = undefined }) @@ -73,7 +77,7 @@ describe('lib/browsers/chrome', () => { }) it('focuses on the page, calls CRI Page.visit, enables Page events, and sets download behavior', function () { - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { expect(utils.getPort).to.have.been.calledOnce // to get remote interface port expect(this.pageCriClient.send.callCount).to.equal(5) @@ -87,7 +91,7 @@ describe('lib/browsers/chrome', () => { }) it('is noop without before:browser:launch', function () { - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { expect(plugins.execute).not.to.be.called }) @@ -101,7 +105,7 @@ describe('lib/browsers/chrome', () => { plugins.execute.resolves(null) - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { // to initialize remote interface client and prepare for true tests // we load the browser with blank page first @@ -112,7 +116,7 @@ describe('lib/browsers/chrome', () => { it('sets default window size and DPR in headless mode', function () { chrome._writeExtension.restore() - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -127,7 +131,7 @@ describe('lib/browsers/chrome', () => { it('does not load extension in headless mode', function () { chrome._writeExtension.restore() - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -158,7 +162,7 @@ describe('lib/browsers/chrome', () => { profilePath, name: 'chromium', channel: 'stable', - }, 'http://', {}, this.automation) + }, 'http://', openOpts, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -177,7 +181,7 @@ describe('lib/browsers/chrome', () => { const onWarning = sinon.stub() - return chrome.open({ isHeaded: true }, 'http://', { onWarning }, this.automation) + return chrome.open({ isHeaded: true }, 'http://', { onWarning, onError: () => {} }, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -201,7 +205,7 @@ describe('lib/browsers/chrome', () => { const pathToTheme = extension.getPathToTheme() - return chrome.open({ isHeaded: true }, 'http://', {}, this.automation) + return chrome.open({ isHeaded: true }, 'http://', openOpts, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -223,7 +227,7 @@ describe('lib/browsers/chrome', () => { const onWarning = sinon.stub() - return chrome.open({ isHeaded: true }, 'http://', { onWarning }, this.automation) + return chrome.open({ isHeaded: true }, 'http://', { onWarning, onError: () => {} }, this.automation) .then(() => { const args = launch.launch.firstCall.args[3] @@ -269,7 +273,7 @@ describe('lib/browsers/chrome', () => { profilePath, name: 'chromium', channel: 'stable', - }, 'http://', {}, this.automation) + }, 'http://', openOpts, this.automation) .then(() => { expect((getFile(fullPath).getMode()) & 0o0700).to.be.above(0o0500) }) @@ -285,7 +289,7 @@ describe('lib/browsers/chrome', () => { sinon.stub(fs, 'outputJson').resolves() - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { expect(fs.outputJson).to.be.calledWith('/profile/dir/Default/Preferences', { profile: { @@ -302,7 +306,7 @@ describe('lib/browsers/chrome', () => { kill, } = this.launchedBrowser - return chrome.open({ isHeadless: true }, 'http://', {}, this.automation) + return chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation) .then(() => { expect(typeof this.launchedBrowser.kill).to.eq('function') @@ -316,7 +320,7 @@ describe('lib/browsers/chrome', () => { it('rejects if CDP version check fails', function () { this.browserCriClient.ensureMinimumProtocolVersion.throws() - return expect(chrome.open({ isHeadless: true }, 'http://', {}, this.automation)).to.be.rejectedWith('Cypress requires at least Chrome 64.') + return expect(chrome.open({ isHeadless: true }, 'http://', openOpts, this.automation)).to.be.rejectedWith('Cypress requires at least Chrome 64.') }) // https://github.com/cypress-io/cypress/issues/9265 @@ -371,6 +375,7 @@ describe('lib/browsers/chrome', () => { describe('adding header to AUT iframe request', function () { const withExperimentalFlagOn = { + ...openOpts, experimentalSessionAndOrigin: true, } @@ -398,7 +403,7 @@ describe('lib/browsers/chrome', () => { }) it('does not listen to Fetch.requestPaused if experimental flag is off', async function () { - await chrome.open('chrome', 'http://', { experimentalSessionAndOrigin: false }, this.automation) + await chrome.open('chrome', 'http://', { ...openOpts, experimentalSessionAndOrigin: false }, this.automation) expect(this.pageCriClient.on).not.to.be.calledWith('Fetch.requestPaused') }) @@ -511,7 +516,7 @@ describe('lib/browsers/chrome', () => { } let onInitializeNewBrowserTabCalled = false - const options = { onError: () => {}, url: 'https://www.google.com', downloadsFolder: '/tmp/folder', onInitializeNewBrowserTab: () => { + const options = { ...openOpts, url: 'https://www.google.com', downloadsFolder: '/tmp/folder', onInitializeNewBrowserTab: () => { onInitializeNewBrowserTabCalled = true } } diff --git a/packages/server/test/unit/browsers/electron_spec.js b/packages/server/test/unit/browsers/electron_spec.js index 802d48415cd7..96f152d61f62 100644 --- a/packages/server/test/unit/browsers/electron_spec.js +++ b/packages/server/test/unit/browsers/electron_spec.js @@ -78,7 +78,7 @@ describe('lib/browsers/electron', () => { context('.connectToNewSpec', () => { it('calls open with the browser, url, options, and automation', async function () { sinon.stub(electron, 'open').withArgs({ isHeaded: true }, 'http://www.example.com', { url: 'http://www.example.com' }, this.automation) - await electron.connectToNewSpec({ isHeaded: true }, 50505, { url: 'http://www.example.com' }, this.automation) + await electron.connectToNewSpec({ isHeaded: true }, { url: 'http://www.example.com' }, this.automation) expect(electron.open).to.be.called }) }) @@ -120,7 +120,8 @@ describe('lib/browsers/electron', () => { expect(this.win.webContents.getOSProcessId).to.be.calledOnce - expect(obj.pid).to.deep.eq([ELECTRON_PID]) + expect(obj.pid).to.eq(ELECTRON_PID) + expect(obj.allPids).to.deep.eq([ELECTRON_PID]) }) }) @@ -722,7 +723,7 @@ describe('lib/browsers/electron', () => { ) }) - it('adds pid of new BrowserWindow to pid list', function () { + it('adds pid of new BrowserWindow to allPids list', function () { const opts = electron._defaultOptions(this.options.projectRoot, this.state, this.options) const NEW_WINDOW_PID = ELECTRON_PID * 2 @@ -739,7 +740,7 @@ describe('lib/browsers/electron', () => { }).then((instance) => { return opts.onNewWindow.call(this.win, {}, this.url) .then(() => { - expect(instance.pid).to.deep.eq([ELECTRON_PID, NEW_WINDOW_PID]) + expect(instance.allPids).to.deep.eq([ELECTRON_PID, NEW_WINDOW_PID]) }) }) }) diff --git a/packages/server/test/unit/open_project_spec.js b/packages/server/test/unit/open_project_spec.js index 44c9d11fcfb6..f209dccb9262 100644 --- a/packages/server/test/unit/open_project_spec.js +++ b/packages/server/test/unit/open_project_spec.js @@ -21,6 +21,7 @@ describe('lib/open_project', () => { this.config = { excludeSpecPattern: '**/*.nope', projectRoot: todosPath, + proxyServer: 'http://cy-proxy-server', } this.onError = sinon.stub() diff --git a/packages/server/test/unit/util/process_profiler_spec.ts b/packages/server/test/unit/util/process_profiler_spec.ts index bd1c965289bc..c2cf9ba005f1 100644 --- a/packages/server/test/unit/util/process_profiler_spec.ts +++ b/packages/server/test/unit/util/process_profiler_spec.ts @@ -187,6 +187,7 @@ describe('lib/util/process_profiler', function () { const result = _aggregateGroups(_groupCyProcesses({ list: processes })) // main process will have variable pid, replace it w constant for snapshotting + // @ts-ignore _.find(result, { pids: String(MAIN_PID) }).pids = '111111111' // @ts-ignore diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index a38eba449acc..fae60c3fae53 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -1,17 +1,26 @@ import type { FoundBrowser } from './browser' +import type { ReceivedCypressOptions } from './config' import type { PlatformName } from './platform' -export interface LaunchOpts { - browser?: FoundBrowser - url?: string - automationMiddleware?: AutomationMiddleware - projectRoot?: string - shouldLaunchNewTab?: boolean +export type OpenProjectLaunchOpts = { + projectRoot: string + shouldLaunchNewTab: boolean + automationMiddleware: AutomationMiddleware + onWarning: (err: Error) => void +} + +export type BrowserLaunchOpts = { + browsers: FoundBrowser[] + browser: FoundBrowser + url: string | undefined + proxyServer: string onBrowserClose?: (...args: unknown[]) => void onBrowserOpen?: (...args: unknown[]) => void onError?: (err: Error) => void - onWarning?: (err: Error) => void -} +} & Partial // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts +& Pick + +export type BrowserNewTabOpts = { onInitializeNewBrowserTab: () => void } & BrowserLaunchOpts export interface LaunchArgs { _: [string] // Cypress App binary location