mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-26 00:49:05 -06:00
chore(server): convert remaining browsers code to typescript (#23556)
This commit is contained in:
@@ -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$')),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
200
packages/server/lib/browsers/index.ts
Normal file
200
packages/server/lib/browsers/index.ts
Normal 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
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
} }
|
||||
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('lib/open_project', () => {
|
||||
this.config = {
|
||||
excludeSpecPattern: '**/*.nope',
|
||||
projectRoot: todosPath,
|
||||
proxyServer: 'http://cy-proxy-server',
|
||||
}
|
||||
|
||||
this.onError = sinon.stub()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user