chore(server): convert remaining browsers code to typescript (#23556)

This commit is contained in:
Zach Bloomquist
2022-08-29 15:47:05 -04:00
committed by GitHub
parent 4871ebc333
commit 3c2fea216b
26 changed files with 597 additions and 611 deletions

View File

@@ -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$')),
)
})
})

View File

@@ -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,
)
})
})

View File

@@ -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<unknown>
launchProject(browser: FoundBrowser, spec: Cypress.Spec, options: LaunchOpts): Promise<void>
launchProject(browser: FoundBrowser, spec: Cypress.Spec, options?: OpenProjectLaunchOpts): Promise<void>
insertProjectToCache(projectRoot: string): Promise<void>
removeProjectFromCache(projectRoot: string): Promise<void>
getProjectRootsFromCache(): Promise<ProjectShape[]>
@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
},

View File

@@ -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 <T>(retryable: () => Promise<T>, 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<BrowserCriClient> {
static async create (port: number, browserName: string, onAsynchronousError: Function, onReconnect?: (client: CriClient) => void): Promise<BrowserCriClient> {
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<CRIWrapper.Client> => {
attachToTargetUrl = async (url: string): Promise<CriClient> => {
// 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

View File

@@ -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<chrome.cookies.Cookie, 'name' | 'value' | 'expirationDate' | 'hostOnly' | 'domain' | 'path' | 'secure' | 'httpOnly'> & {
@@ -163,9 +168,9 @@ export const normalizeResourceType = (resourceType: string | undefined): Resourc
return ffToStandardResourceTypeMap[resourceType] || 'other'
}
type SendDebuggerCommand = (message: string, data?: any) => Promise<any>
type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise<any>
type OnFn = (eventName: string, cb: Function) => void
type SendDebuggerCommand = (message: CdpCommand, data?: any) => Promise<any>
type SendCloseCommand = (shouldKeepTabOpen: boolean) => Promise<any> | 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

View File

@@ -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<void>(async (resolve) => {
@@ -433,8 +431,8 @@ const _handlePausedRequests = async (client) => {
})
}
const _setAutomation = async (client: CRIWrapper.Client, automation: Automation, resetBrowserTargets: (shouldKeepTabOpen: boolean) => Promise<void>, 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<void>, 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<LaunchedBrowser> {
async open (browser: Browser, url, options: BrowserLaunchOpts, automation: Automation): Promise<LaunchedBrowser> {
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

View File

@@ -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<any>
/**
* 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<void>
}
targetId: string
/**
* Sends a command to the Chrome remote interface.
* @example client.send('Page.navigate', { url })
*/
send (command: CdpCommand, params?: object): Promise<any>
/**
* 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<void>
}
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<CRIWrapper.Client> => {
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<CriClient> => {
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 } })

View File

@@ -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()
}
})
const child = await _this._launchChild(e, url, _win, projectRoot, state, options, automation)
// add this pid to list of pids
tryToCall(child, () => {
if (instance && instance.pid) {
instance.pid.push(child.webContents.getOSProcessId())
}
})
// 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, '')
this._setUserAgent(win.webContents, modifiedNonObstructiveUserAgent)
}
const setProxy = () => {
let ps
ps = options.proxyServer
if (ps) {
return this._setProxy(win.webContents, ps)
}
}
const setProxy = () => {
let ps
await Promise.all([
setProxy(),
this._clearCache(win.webContents),
])
ps = options.proxyServer
await win.loadURL('about:blank')
const cdpAutomation = await this._getAutomation(win, options, automation)
if (ps) {
return this._setProxy(win.webContents, ps)
}
}
automation.use(cdpAutomation)
await Promise.all([
_maybeRecordVideo(win.webContents, options),
this._handleDownloads(win, options.downloadsFolder, automation),
])
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)
}
})
.return(win)
// 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
debug('launching browser window to url: %s', url)
return this._render(url, automation, preferences, {
projectRoot: options.projectRoot,
isTextTerminal: options.isTextTerminal,
})
.then(async (win) => {
await _installExtensions(win, launchOptions.extensions, options)
// 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 events = new EE
win.once('closed', () => {
debug('closed event fired')
Windows.removeAllExtensions(win)
return events.emit('exit')
})
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')
},
})
return instance
})
const defaultLaunchOptions = utils.getDefaultLaunchOptions({
preferences: options,
})
const launchOptions = await utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options)
const { preferences } = launchOptions
debug('launching browser window to url: %s', url)
const win = await this._render(url, automation, preferences, {
projectRoot: options.projectRoot,
isTextTerminal: options.isTextTerminal,
})
await _installExtensions(win, launchOptions.extensions, options)
// 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 events = new EE()
win.once('closed', () => {
debug('closed event fired')
Windows.removeAllExtensions(win)
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
},
}

View File

@@ -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,
}

View File

@@ -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<void>((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<BrowserLauncher> {
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<BrowserInstance | null> {
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<BrowserInstance | null> {
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<BrowserInstance | null> {
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

View File

@@ -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<BrowserInstance>
connectToNewSpec: (browser: Browser, options: BrowserNewTabOpts, automation: Automation) => Promise<void>
/**
* Used in Cypress-in-Cypress tests to connect to the existing browser instance.
*/
connectToExisting: (browser: Browser, options: BrowserLaunchOpts, automation: Automation) => void | Promise<void>
}

View File

@@ -293,8 +293,8 @@ const parseBrowserOption = (opt) => {
}
}
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers: FoundBrowser[]): Bluebird<FoundBrowser>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers: FoundBrowser[]): Bluebird<FoundBrowser[]>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: false, browsers?: FoundBrowser[]): Bluebird<FoundBrowser>
function ensureAndGetByNameOrPath(nameOrPath: string, returnAll: true, browsers?: FoundBrowser[]): Bluebird<FoundBrowser[]>
async function ensureAndGetByNameOrPath (nameOrPath: string, returnAll = false, prevKnownBrowsers: FoundBrowser[] = []) {
const browsers = prevKnownBrowsers.length ? prevKnownBrowsers : (await getBrowsers())

View File

@@ -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<BrowserInstance> {
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<BrowserInstance> {
// resolve pw from user's project path
const pwModulePath = require.resolve('playwright-webkit', { paths: [process.cwd()] })
const pw = require(pwModulePath) as typeof playwright

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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<any> | null = null
relaunchBrowser: ((...args: unknown[]) => Bluebird<void>) | null = null
relaunchBrowser: (() => Promise<any>) | 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)
}
// 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 (options.shouldLaunchNewTab) {
const onInitializeNewBrowserTab = async () => {
await this.resetBrowserState()
}
if (options.shouldLaunchNewTab) {
const onInitializeNewBrowserTab = async () => {
await this.resetBrowserState()
}
// 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)
}
// 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)
}
return browsers.open(browser, options, automation, this._ctx)
})
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) {

View File

@@ -296,14 +296,6 @@ export class ProjectBase<TServer extends Server> extends EE {
return runEvents.execute('after:run', config)
}
_onError<Options extends Record<string, any>> (err: Error, options: Options) {
debug('got plugins error', err.stack)
browsers.close()
options.onError(err)
}
initializeReporter ({
report,
reporter,

View File

@@ -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')
}

View File

@@ -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
} }

View File

@@ -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])
})
})
})

View File

@@ -21,6 +21,7 @@ describe('lib/open_project', () => {
this.config = {
excludeSpecPattern: '**/*.nope',
projectRoot: todosPath,
proxyServer: 'http://cy-proxy-server',
}
this.onError = sinon.stub()

View File

@@ -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

View File

@@ -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<OpenProjectLaunchOpts> // TODO: remove the `Partial` here by making it impossible for openProject.launch to be called w/o OpenProjectLaunchOpts
& Pick<ReceivedCypressOptions, 'userAgent' | 'proxyUrl' | 'socketIoRoute' | 'chromeWebSecurity' | 'isTextTerminal' | 'downloadsFolder' | 'experimentalSessionAndOrigin' | 'experimentalModifyObstructiveThirdPartyCode'>
export type BrowserNewTabOpts = { onInitializeNewBrowserTab: () => void } & BrowserLaunchOpts
export interface LaunchArgs {
_: [string] // Cypress App binary location