feat: Close and reopen a new tab in between tests to get around a memory leak (#19915)

This commit is contained in:
Ryan Manuel
2022-02-08 10:16:03 -06:00
committed by GitHub
parent ebb387938c
commit 655090330f
38 changed files with 1307 additions and 679 deletions
+5 -1
View File
@@ -459,7 +459,6 @@ commands:
command: |
cmd=$([[ <<parameters.percy>> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true
DEBUG=<<parameters.debug>> \
CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH='true' \
CYPRESS_KONFIG_ENV=production \
CYPRESS_RECORD_KEY=$TEST_LAUNCHPAD_RECORD_KEY \
PERCY_PARALLEL_NONCE=$PLATFORM-$CIRCLE_SHA1 \
@@ -1218,6 +1217,7 @@ jobs:
run-frontend-shared-component-tests-chrome:
<<: *defaults
resource_class: medium
parallelism: 1
steps:
- run-new-ui-tests:
@@ -1228,6 +1228,7 @@ jobs:
run-launchpad-component-tests-chrome:
<<: *defaults
resource_class: medium
parallelism: 7
steps:
- run-new-ui-tests:
@@ -1239,6 +1240,7 @@ jobs:
run-launchpad-integration-tests-chrome:
<<: *defaults
resource_class: medium
parallelism: 2
steps:
- run-new-ui-tests:
@@ -1249,6 +1251,7 @@ jobs:
run-app-component-tests-chrome:
<<: *defaults
resource_class: medium
parallelism: 7
steps:
- run-new-ui-tests:
@@ -1259,6 +1262,7 @@ jobs:
run-app-integration-tests-chrome:
<<: *defaults
resource_class: medium
parallelism: 2
steps:
- run-new-ui-tests:
+19
View File
@@ -88,6 +88,10 @@ const connect = function (host, path, extraOpts) {
return invoke('focus', id)
case 'take:screenshot':
return invoke('takeScreenshot', id)
case 'reset:browser:state':
return invoke('resetBrowserState', id)
case 'close:browser:tabs':
return invoke('closeBrowserTabs', id)
default:
return fail(id, { message: `No handler registered for: '${msg}'` })
}
@@ -205,6 +209,21 @@ const automation = {
}).then(fn)
},
resetBrowserState (fn) {
// We remove browser data. Firefox goes through this path, while chrome goes through cdp automation
// Note that firefox does not support fileSystems or serverBoundCertificates
// (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData/DataTypeSet).
return browser.browsingData.remove({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).then(fn)
},
closeBrowserTabs (fn) {
return Promise.try(() => {
return browser.windows.getCurrent({ populate: true })
}).then((windowInfo) => {
return browser.tabs.remove(windowInfo.tabs.map((tab) => tab.id))
}).then(fn)
},
query (data) {
const code = `var s; (s = document.getElementById('${data.element}')) && s.textContent`
+1
View File
@@ -8,6 +8,7 @@
},
"permissions": [
"cookies",
"browsingData",
"downloads",
"tabs",
"http://*/*",
@@ -24,6 +24,8 @@ const browser = {
},
windows: {
getLastFocused () {},
getCurrent () {},
update () {},
},
runtime: {
@@ -32,6 +34,10 @@ const browser = {
query () {},
executeScript () {},
captureVisibleTab () {},
remove () {},
},
browsingData: {
remove () {},
},
}
@@ -663,6 +669,27 @@ describe('app/background', () => {
})
})
describe('focus:browser:window', () => {
beforeEach(() => {
sinon.stub(browser.windows, 'getCurrent').resolves({ id: '10' })
sinon.stub(browser.windows, 'update').withArgs('10', { focused: true }).resolves()
})
it('focuses the current window', function (done) {
this.socket.on('automation:response', (id, obj = {}) => {
expect(id).to.eq(123)
expect(obj.response).to.be.undefined
expect(browser.windows.getCurrent).to.be.called
expect(browser.windows.update).to.be.called
done()
})
return this.server.emit('automation:request', 123, 'focus:browser:window')
})
})
describe('take:screenshot', () => {
beforeEach(() => {
return sinon.stub(browser.windows, 'getLastFocused').resolves({ id: 1 })
@@ -700,5 +727,45 @@ describe('app/background', () => {
return this.server.emit('automation:request', 123, 'take:screenshot')
})
})
describe('reset:browser:state', () => {
beforeEach(() => {
sinon.stub(browser.browsingData, 'remove').withArgs({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).resolves()
})
it('resets the browser state', function (done) {
this.socket.on('automation:response', (id, obj) => {
expect(id).to.eq(123)
expect(obj.response).to.be.undefined
expect(browser.browsingData.remove).to.be.called
done()
})
return this.server.emit('automation:request', 123, 'reset:browser:state')
})
})
describe('close:browser:tabs', () => {
beforeEach(() => {
sinon.stub(browser.windows, 'getCurrent').withArgs({ populate: true }).resolves({ id: '10', tabs: [{ id: '1' }, { id: '2' }, { id: '3' }] })
sinon.stub(browser.tabs, 'remove').withArgs(['1', '2', '3']).resolves()
})
it('closes the tabs in the current browser window', function (done) {
this.socket.on('automation:response', (id, obj) => {
expect(id).to.eq(123)
expect(obj.response).to.be.undefined
expect(browser.windows.getCurrent).to.be.called
expect(browser.tabs.remove).to.be.called
done()
})
return this.server.emit('automation:request', 123, 'close:browser:tabs')
})
})
})
})
+6 -1
View File
@@ -1,18 +1,23 @@
import { log } from './log'
import * as cp from 'child_process'
import { browsers, FoundBrowser } from '@packages/types'
import type { Readable } from 'stream'
export { browsers }
/** list of the browsers we can detect and use by default */
/** starts a found browser and opens URL if given one */
export type LaunchedBrowser = cp.ChildProcessByStdio<null, Readable, Readable>
export function launch (
browser: FoundBrowser,
url: string,
debuggingPort: number,
args: string[] = [],
defaultBrowserEnv = {},
) {
): LaunchedBrowser {
log('launching browser %o', { browser, url })
if (!browser.path) {
@@ -0,0 +1,168 @@
import CRI from 'chrome-remote-interface'
import Debug from 'debug'
import { _connectAsync, _getDelayMsForRetry } from './protocol'
import errors from '../errors'
import { create, CRIWrapper } from './cri-client'
const HOST = '127.0.0.1'
const debug = Debug('cypress:server:browsers:browser-cri-client')
interface Version {
major: number
minor: number
}
const isVersionGte = (a: Version, b: Version) => {
return a.major > b.major || (a.major === b.major && a.minor >= b.minor)
}
const getMajorMinorVersion = (version: string): Version => {
const [major, minor] = version.split('.', 2).map(Number)
return { major, minor }
}
const ensureLiveBrowser = async (port: number, browserName: string) => {
const connectOpts = {
host: HOST,
port,
getDelayMsForRetry: (i) => {
return _getDelayMsForRetry(i, browserName)
},
}
try {
await _connectAsync(connectOpts)
} catch (err) {
debug('failed to connect to CDP %o', { connectOpts, err })
errors.throw('CDP_COULD_NOT_CONNECT', port, err, browserName)
}
}
export class BrowserCriClient {
private currentlyAttachedTarget: CRIWrapper.Client | undefined
private constructor (private browserClient: CRIWrapper.Client, private versionInfo, private port: number, private onAsynchronousError: Function) {}
/**
* Factory method for the browser cri client. Connects to the browser and then returns a chrome remote interface wrapper around the
* browser target
*
* @param port the port to which to connect
* @param browserName the display name of the browser being launched
* @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): Promise<BrowserCriClient> {
await ensureLiveBrowser(port, browserName)
let retryIndex = 0
const retry = async (): Promise<BrowserCriClient> => {
debug('attempting to find CRI target... %o', { retryIndex })
try {
const versionInfo = await CRI.Version({ host: HOST, port })
const browserClient = await create(versionInfo.webSocketDebuggerUrl, onAsynchronousError)
return new BrowserCriClient(browserClient, versionInfo, port, onAsynchronousError)
} catch (err) {
retryIndex++
const delay = _getDelayMsForRetry(retryIndex, browserName)
debug('error finding CRI target, maybe retrying %o', { delay, err })
if (typeof delay === 'undefined') {
throw err
}
await new Promise((resolve) => setTimeout(resolve, delay))
return retry()
}
}
return retry()
}
/**
* Ensures that the minimum protocol version for the browser is met
*
* @param protocolVersion the minimum version to ensure
*/
ensureMinimumProtocolVersion = (protocolVersion: string): void => {
const actualVersion = getMajorMinorVersion(this.versionInfo['Protocol-Version'])
const minimum = getMajorMinorVersion(protocolVersion)
if (!isVersionGte(actualVersion, minimum)) {
errors.throw('CDP_VERSION_TOO_OLD', protocolVersion, actualVersion)
}
}
/**
* Attaches to a target with the given url
*
* @param url the url to attach to
* @returns the chrome remote interface wrapper for the target
*/
attachToTargetUrl = async (url: string): Promise<CRIWrapper.Client> => {
debug('Attaching to target url %s', url)
const { targetInfos: targets } = await this.browserClient.send('Target.getTargets')
const target = targets.find((target) => target.url === url)
// TODO: Move this into packages/error post merge: https://github.com/cypress-io/cypress/pull/20072
if (!target) {
throw new Error(`Could not find url target in browser ${url}. Targets were ${JSON.stringify(targets)}`)
}
this.currentlyAttachedTarget = await create(target.targetId, this.onAsynchronousError, HOST, this.port)
return this.currentlyAttachedTarget
}
/**
* Creates a new target with the given url and then attaches to it
*
* @param url the url to create and attach to
* @returns the chrome remote interface wrapper for the target
*/
attachToNewUrl = async (url: string): Promise<CRIWrapper.Client> => {
debug('Attaching to new url %s', url)
const target = await this.browserClient.send('Target.createTarget', { url })
this.currentlyAttachedTarget = await create(target.targetId, this.onAsynchronousError, HOST, this.port)
return this.currentlyAttachedTarget
}
/**
* Closes the currently attached page target
*/
closeCurrentTarget = async (): Promise<void> => {
// TODO: Move this into packages/error post merge: https://github.com/cypress-io/cypress/pull/20072
if (!this.currentlyAttachedTarget) {
throw new Error('Cannot close target because no target is currently attached')
}
debug('Closing current target %s', this.currentlyAttachedTarget.targetId)
await Promise.all([
// If this fails, it shouldn't prevent us from continuing
this.currentlyAttachedTarget.close().catch(),
this.browserClient.send('Target.closeTarget', { targetId: this.currentlyAttachedTarget.targetId }),
])
this.currentlyAttachedTarget = undefined
}
/**
* Closes the browser client socket as well as the socket for the currently attached page target
*/
close = async () => {
if (this.currentlyAttachedTarget) {
await this.currentlyAttachedTarget.close()
}
await this.browserClient.close()
}
}
+24 -9
View File
@@ -161,7 +161,8 @@ const normalizeResourceType = (resourceType: string | undefined): ResourceType =
return ffToStandardResourceTypeMap[resourceType] || 'other'
}
type SendDebuggerCommand = (message: string, data?: any) => Bluebird<any>
type SendDebuggerCommand = (message: string, data?: any) => Promise<any>
type SendCloseCommand = () => Promise<any>
type OnFn = (eventName: string, cb: Function) => void
// the intersection of what's valid in CDP and what's valid in FFCDP
@@ -175,14 +176,21 @@ const ffToStandardResourceTypeMap: { [ff: string]: ResourceType } = {
}
export class CdpAutomation {
constructor (private sendDebuggerCommandFn: SendDebuggerCommand, onFn: OnFn, private automation: Automation) {
private constructor (private sendDebuggerCommandFn: SendDebuggerCommand, private onFn: OnFn, private sendCloseCommandFn: SendCloseCommand, private automation: Automation) {
onFn('Network.requestWillBeSent', this.onNetworkRequestWillBeSent)
onFn('Network.responseReceived', this.onResponseReceived)
sendDebuggerCommandFn('Network.enable', {
}
static async create (sendDebuggerCommandFn: SendDebuggerCommand, onFn: OnFn, sendCloseCommandFn: SendCloseCommand, automation: Automation): Promise<CdpAutomation> {
const cdpAutomation = new CdpAutomation(sendDebuggerCommandFn, onFn, sendCloseCommandFn, automation)
await sendDebuggerCommandFn('Network.enable', {
maxTotalBufferSize: 0,
maxResourceBufferSize: 0,
maxPostDataSize: 0,
})
return cdpAutomation
}
private onNetworkRequestWillBeSent = (params: Protocol.Network.RequestWillBeSentEvent) => {
@@ -230,7 +238,7 @@ export class CdpAutomation {
})
}
private getCookiesByUrl = (url): Bluebird<CyCookie[]> => {
private getCookiesByUrl = (url): Promise<CyCookie[]> => {
return this.sendDebuggerCommandFn('Network.getCookies', {
urls: [url],
})
@@ -242,7 +250,7 @@ export class CdpAutomation {
})
}
private getCookie = (filter: CyCookieFilter): Bluebird<CyCookie | null> => {
private getCookie = (filter: CyCookieFilter): Promise<CyCookie | null> => {
return this.getAllCookies(filter)
.then((cookies) => {
return _.get(cookies, 0, null)
@@ -285,15 +293,15 @@ export class CdpAutomation {
case 'clear:cookie':
return this.getCookie(data)
// tap, so we can resolve with the value of the removed cookie
// resolve with the value of the removed cookie
// also, getting the cookie via CDP first will ensure that we send a cookie `domain` to CDP
// that matches the cookie domain that is really stored
.tap((cookieToBeCleared) => {
.then((cookieToBeCleared) => {
if (!cookieToBeCleared) {
return
return cookieToBeCleared
}
return this.sendDebuggerCommandFn('Network.deleteCookies', _.pick(cookieToBeCleared, 'name', 'domain'))
return this.sendDebuggerCommandFn('Network.deleteCookies', _.pick(cookieToBeCleared, 'name', 'domain')).then(() => cookieToBeCleared)
})
case 'clear:cookies':
@@ -322,6 +330,13 @@ export class CdpAutomation {
.then(({ data }) => {
return `data:image/png;base64,${data}`
})
case 'reset:browser:state':
return Promise.all([
this.sendDebuggerCommandFn('Storage.clearDataForOrigin', { origin: '*', storageTypes: 'all' }),
this.sendDebuggerCommandFn('Network.clearBrowserCache'),
])
case 'close:browser:tabs':
return this.sendCloseCommandFn()
case 'focus:browser:window':
return this.sendDebuggerCommandFn('Page.bringToFront')
default:
+61 -41
View File
@@ -12,10 +12,13 @@ import { launch } from '@packages/launcher'
import appData from '../util/app_data'
import { fs } from '../util/fs'
import { CdpAutomation, screencastOpts } from './cdp_automation'
import * as CriClient from './cri-client'
import * as protocol from './protocol'
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 { Automation } from '../automation'
// TODO: this is defined in `cypress-npm-api` but there is currently no way to get there
type CypressConfiguration = any
@@ -119,6 +122,8 @@ const DEFAULT_ARGS = [
'--disable-dev-shm-usage',
]
let browserCriClient
/**
* Reads all known preference files (CHROME_PREFERENCE_PATHS) from disk and retur
* @param userDir
@@ -245,22 +250,6 @@ const _disableRestorePagesPrompt = function (userDir) {
.catch(() => { })
}
// After the browser has been opened, we can connect to
// its remote interface via a websocket.
const _connectToChromeRemoteInterface = function (port, onError, browserDisplayName, url?) {
// @ts-ignore
la(check.userPort(port), 'expected port number to connect CRI to', port)
debug('connecting to Chrome remote interface at random port %d', port)
return protocol.getWsTargetFor(port, browserDisplayName, url)
.then((wsUrl) => {
debug('received wsUrl %s for port %d', wsUrl, port)
return CriClient.create(wsUrl, onError)
})
}
const _maybeRecordVideo = async function (client, options, browserMajorVersion) {
if (!options.onScreencastFrame) {
debug('options.onScreencastFrame is false')
@@ -329,10 +318,10 @@ const _handleDownloads = async function (client, dir, automation) {
})
}
const _setAutomation = (client, automation) => {
return automation.use(
new CdpAutomation(client.send, client.on, automation),
)
const _setAutomation = async (client: CRIWrapper.Client, automation: Automation, closeCurrentTarget: () => Promise<void>) => {
const cdpAutomation = await CdpAutomation.create(client.send, client.on, closeCurrentTarget, automation)
return automation.use(cdpAutomation)
}
export = {
@@ -346,8 +335,6 @@ export = {
_removeRootExtension,
_connectToChromeRemoteInterface,
_maybeRecordVideo,
_navigateUsingCRI,
@@ -362,6 +349,10 @@ export = {
_writeChromePreferences,
_getBrowserCriClient () {
return browserCriClient
},
async _writeExtension (browser: Browser, options) {
if (browser.isHeadless) {
debug('chrome is running headlessly, not installing extension')
@@ -443,14 +434,32 @@ export = {
return args
},
async connectToExisting (browser: Browser, options: CypressConfiguration = {}, automation) {
const port = await protocol.getRemoteDebuggingPort()
const criClient = await this._connectToChromeRemoteInterface(port, options, browser.displayName, options.url)
async connectToNewSpec (browser: Browser, options: CypressConfiguration = {}, automation: Automation) {
debug('connecting to new chrome tab in existing instance with url and debugging port', { url: options.url })
this._setAutomation(criClient, automation)
const browserCriClient = this._getBrowserCriClient()
const pageCriClient = await browserCriClient.attachToNewUrl('about:blank')
await this._setAutomation(pageCriClient, automation, browserCriClient.closeCurrentTarget)
await options.onInitializeNewBrowserTab()
await this._maybeRecordVideo(pageCriClient, options, browser.majorVersion)
await this._navigateUsingCRI(pageCriClient, options.url)
await this._handleDownloads(pageCriClient, options.downloadsFolder, automation)
},
async open (browser: Browser, url, options: CypressConfiguration = {}, automation) {
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)
const pageCriClient = await browserCriClient.attachToTargetUrl(options.url)
await this._setAutomation(pageCriClient, automation, browserCriClient.closeCurrentTarget)
},
async open (browser: Browser, url, options: CypressConfiguration = {}, automation: Automation): Promise<LaunchedBrowser> {
const { isTextTerminal } = options
const userDir = utils.getProfileDir(browser, isTextTerminal)
@@ -502,43 +511,54 @@ export = {
// first allows us to connect the remote interface,
// start video recording and then
// we will load the actual page
const launchedBrowser = await launch(browser, 'about:blank', args)
const launchedBrowser = await launch(browser, 'about:blank', port, args) as LaunchedBrowser & { browserCriClient: BrowserCriClient}
la(launchedBrowser, 'did not get launched browser instance')
// SECOND connect to the Chrome remote interface
// and when the connection is ready
// navigate to the actual url
const criClient = await this._connectToChromeRemoteInterface(port, options.onError, browser.displayName)
browserCriClient = await BrowserCriClient.create(port, browser.displayName, options.onError)
la(criClient, 'expected Chrome remote interface reference', criClient)
la(browserCriClient, 'expected Chrome remote interface reference', browserCriClient)
await criClient.ensureMinimumProtocolVersion('1.3')
.catch((err) => {
// if this minumum chrome version changes, sync it with
try {
browserCriClient.ensureMinimumProtocolVersion('1.3')
} catch (err: any) {
// if this minimum chrome version changes, sync it with
// packages/web-config/webpack.config.base.ts and
// npm/webpack-batteries-included-preprocessor/index.js
throw new Error(`Cypress requires at least Chrome 64.\n\nDetails:\n${err.message}`)
})
this._setAutomation(criClient, automation)
}
// monkey-patch the .kill method to that the CDP connection is closed
const originalBrowserKill = launchedBrowser.kill
launchedBrowser.browserCriClient = browserCriClient
/* @ts-expect-error */
launchedBrowser.kill = (...args) => {
debug('closing remote interface client')
criClient.close()
// Do nothing on failure here since we're shutting down anyway
browserCriClient.close().catch()
browserCriClient = undefined
debug('closing chrome')
originalBrowserKill.apply(launchedBrowser, args)
}
await this._maybeRecordVideo(criClient, options, browser.majorVersion)
await this._navigateUsingCRI(criClient, url)
await this._handleDownloads(criClient, options.downloadsFolder, automation)
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
await this._setAutomation(pageCriClient, automation, browserCriClient.closeCurrentTarget)
await Promise.all([
this._maybeRecordVideo(pageCriClient, options, browser.majorVersion),
this._handleDownloads(pageCriClient, options.downloadsFolder, automation),
])
await this._navigateUsingCRI(pageCriClient, url)
// return the launched browser process
// with additional method to close the remote connection
+45 -96
View File
@@ -1,9 +1,7 @@
import Bluebird from 'bluebird'
import debugModule from 'debug'
import _ from 'lodash'
const chromeRemoteInterface = require('chrome-remote-interface')
const errors = require('../errors')
import CRI from 'chrome-remote-interface'
import errors from '../errors'
const debug = debugModule('cypress:server:browsers:cri-client')
// debug using cypress-verbose:server:browsers:cri-client:send:*
@@ -13,16 +11,11 @@ const debugVerboseReceive = debugModule('cypress-verbose:server:browsers:cri-cli
const WEBSOCKET_NOT_OPEN_RE = /^WebSocket is (?:not open|already in CLOSING or CLOSED state)/
/**
* Url returned by the Chrome Remote Interface
*/
type websocketUrl = string
/**
* Enumerations to make programming CDP slightly simpler - provides
* IntelliSense whenever you use named types.
*/
namespace CRI {
export namespace CRIWrapper {
export type Command =
'Browser.getVersion' |
'Page.bringToFront' |
@@ -38,51 +31,31 @@ namespace CRI {
'Page.downloadWillBegin' |
'Page.downloadProgress' |
string
}
/**
* Wrapper for Chrome Remote Interface client. Only allows "send" method.
* @see https://github.com/cyrus-and/chrome-remote-interface#clientsendmethod-params-callback
*/
interface CRIWrapper {
/**
* Get the `protocolVersion` supported by the browser.
* Wrapper for Chrome Remote Interface client. Only allows "send" method.
* @see https://github.com/cyrus-and/chrome-remote-interface#clientsendmethod-params-callback
*/
getProtocolVersion (): Bluebird<Version>
/**
* Rejects if `protocolVersion` is less than the current version.
* @param protocolVersion CDP version string (ex: 1.3)
*/
ensureMinimumProtocolVersion(protocolVersion: string): Bluebird<void>
/**
* Sends a command to the Chrome remote interface.
* @example client.send('Page.navigate', { url })
*/
send (command: CRI.Command, params?: object): Bluebird<any>
/**
* Registers callback for particular event.
* @see https://github.com/cyrus-and/chrome-remote-interface#class-cdp
*/
on (eventName: CRI.EventName, cb: Function): void
/**
* Calls underlying remote interface client close
*/
close (): Bluebird<void>
}
interface Version {
major: number
minor: number
}
const isVersionGte = (a: Version, b: Version) => {
return a.major > b.major || (a.major === b.major && a.minor >= b.minor)
}
const getMajorMinorVersion = (version: string): Version => {
const [major, minor] = version.split('.', 2).map(Number)
return { major, minor }
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>
}
}
const maybeDebugCdpMessages = (cri) => {
@@ -126,26 +99,17 @@ const maybeDebugCdpMessages = (cri) => {
}
}
/**
* Creates a wrapper for Chrome remote interface client
* that only allows to use low-level "send" method
* and not via domain objects and commands.
*
* @example create('ws://localhost:...').send('Page.bringToFront')
*/
export { chromeRemoteInterface }
type DeferredPromise = { resolve: Function, reject: Function }
export const create = Bluebird.method((target: websocketUrl, onAsynchronousError: Function): Bluebird<CRIWrapper> => {
const subscriptions: {eventName: CRI.EventName, cb: Function}[] = []
let enqueuedCommands: {command: CRI.Command, params: any, p: DeferredPromise }[] = []
export const create = (target: string, onAsynchronousError: Function, host?: string, port?: number): Promise<CRIWrapper.Client> => {
const subscriptions: {eventName: CRIWrapper.EventName, cb: Function}[] = []
let enqueuedCommands: {command: CRIWrapper.Command, 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
let client: CRIWrapper.Client
const reconnect = () => {
debug('disconnected, attempting to reconnect... %o', { closed })
@@ -153,6 +117,8 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
connected = false
if (closed) {
enqueuedCommands = []
return
}
@@ -171,7 +137,11 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
enqueuedCommands = []
})
.catch((err) => {
onAsynchronousError(errors.get('CDP_COULD_NOT_RECONNECT', err))
const cdpError = errors.get('CDP_COULD_NOT_RECONNECT', err)
// If we cannot reconnect to CDP, we will be unable to move to the next set of specs since we use CDP to clean up and close tabs. Marking this as fatal
cdpError.isFatalApiErr = true
onAsynchronousError(cdpError)
})
}
@@ -180,7 +150,9 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
debug('connecting %o', { target })
return chromeRemoteInterface({
return CRI({
host,
port,
target,
local: true,
})
@@ -190,8 +162,6 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
maybeDebugCdpMessages(cri)
cri.send = Bluebird.promisify(cri.send, { context: cri })
// @see https://github.com/cyrus-and/chrome-remote-interface/issues/72
cri._notifier.on('disconnect', reconnect)
})
@@ -199,32 +169,11 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
return connect()
.then(() => {
const ensureMinimumProtocolVersion = (protocolVersion: string) => {
return getProtocolVersion()
.then((actual) => {
const minimum = getMajorMinorVersion(protocolVersion)
if (!isVersionGte(actual, minimum)) {
errors.throw('CDP_VERSION_TOO_OLD', protocolVersion, actual)
}
})
}
const getProtocolVersion = _.memoize(() => {
return client.send('Browser.getVersion')
// could be any version <= 1.2
.catchReturn({ protocolVersion: '0.0' })
.then(({ protocolVersion }) => {
return getMajorMinorVersion(protocolVersion)
})
})
client = {
ensureMinimumProtocolVersion,
getProtocolVersion,
send: Bluebird.method((command: CRI.Command, params?: object) => {
targetId: target,
send: async (command, params?) => {
const enqueue = () => {
return new Bluebird((resolve, reject) => {
return new Promise((resolve, reject) => {
enqueuedCommands.push({ command, params, p: { resolve, reject } })
})
}
@@ -247,8 +196,8 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
}
return enqueue()
}),
on (eventName: CRI.EventName, cb: Function) {
},
on (eventName, cb) {
subscriptions.push({ eventName, cb })
debug('registering CDP on event %o', { eventName })
@@ -263,4 +212,4 @@ export const create = Bluebird.method((target: websocketUrl, onAsynchronousError
return client
})
})
}
+12 -4
View File
@@ -36,7 +36,7 @@ const tryToCall = function (win, method) {
}
}
const _getAutomation = function (win, options, parent) {
const _getAutomation = async function (win, options, parent) {
const sendCommand = Bluebird.method((...args) => {
return tryToCall(win, () => {
return win.webContents.debugger.sendCommand
@@ -52,7 +52,11 @@ const _getAutomation = function (win, options, parent) {
})
}
const automation = new CdpAutomation(sendCommand, on, parent)
const sendClose = () => {
win.destroy()
}
const automation = await CdpAutomation.create(sendCommand, on, sendClose, parent)
automation.onRequest = _.wrap(automation.onRequest, async (fn, message, data) => {
switch (message) {
@@ -180,7 +184,7 @@ module.exports = {
_getAutomation,
_render (url, automation, preferences = {}, options = {}) {
async _render (url, automation, preferences = {}, options = {}) {
const win = Windows.create(options.projectRoot, preferences)
if (preferences.browser.isHeadless) {
@@ -195,7 +199,7 @@ module.exports = {
return this._launch(win, url, automation, preferences)
.tap(_maybeRecordVideo(win.webContents, preferences))
.tap(() => automation.use(_getAutomation(win, preferences, automation)))
.tap(() => _getAutomation(win, preferences, automation).then((cdpAutomation) => automation.use(cdpAutomation)))
},
_launchChild (e, url, parent, projectRoot, state, options, automation) {
@@ -389,6 +393,10 @@ module.exports = {
})
},
async connectToNewSpec (browser, options, automation) {
this.open(browser, options.url, options, automation)
},
async connectToExisting () {
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for electron')
},
+56 -11
View File
@@ -7,7 +7,8 @@ import util from 'util'
import Foxdriver from '@benmalka/foxdriver'
import * as protocol from './protocol'
import { CdpAutomation } from './cdp_automation'
import * as CriClient from './cri-client'
import { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
const errors = require('../errors')
@@ -101,11 +102,52 @@ const attachToTabMemory = Bluebird.method((tab) => {
})
})
async function setupRemote (remotePort, automation, onError) {
const wsUrl = await protocol.getWsTargetFor(remotePort, 'Firefox')
const criClient = await CriClient.create(wsUrl, onError)
async function connectMarionetteToNewTab () {
// When firefox closes its last tab, it keeps a blank tab open. This will be the only handle
// So we will connect to it and navigate it to about:blank to set it up for CDP connection
const handles = await sendMarionette({
name: 'WebDriver:GetWindowHandles',
})
new CdpAutomation(criClient.send, criClient.on, automation)
await sendMarionette({
name: 'WebDriver:SwitchToWindow',
parameters: { handle: handles[0] },
})
await navigateToUrl('about:blank')
}
async function connectToNewSpec (options, automation: Automation, browserCriClient: BrowserCriClient) {
debug('firefox: reconnecting to blank tab')
await connectMarionetteToNewTab()
debug('firefox: reconnecting CDP')
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
await CdpAutomation.create(pageCriClient.send, pageCriClient.on, browserCriClient.closeCurrentTarget, automation)
await options.onInitializeNewBrowserTab()
debug(`firefox: navigating to ${options.url}`)
await navigateToUrl(options.url)
}
async function setupRemote (remotePort, automation, onError): Promise<BrowserCriClient> {
const browserCriClient = await BrowserCriClient.create(remotePort, 'Firefox', onError)
const pageCriClient = await browserCriClient.attachToTargetUrl('about:blank')
await CdpAutomation.create(pageCriClient.send, pageCriClient.on, browserCriClient.closeCurrentTarget, automation)
return browserCriClient
}
async function navigateToUrl (url) {
await sendMarionette({
name: 'WebDriver:Navigate',
parameters: { url },
})
}
const logGcDetails = () => {
@@ -180,14 +222,20 @@ export default {
marionettePort,
foxdriverPort,
remotePort,
}) {
}): Bluebird<BrowserCriClient> {
return Bluebird.all([
this.setupFoxdriver(foxdriverPort),
this.setupMarionette(extensions, url, marionettePort),
remotePort && setupRemote(remotePort, automation, onError),
])
]).then(([,, browserCriClient]) => browserCriClient)
},
connectToNewSpec,
navigateToUrl,
setupRemote,
async setupFoxdriver (port) {
await protocol._connectAsync({
host: '127.0.0.1',
@@ -305,10 +353,7 @@ export default {
}))
})
.then(() => {
return sendMarionette({
name: 'WebDriver:Navigate',
parameters: { url },
})
return navigateToUrl(url)
})
.then(resolve)
.catch(_onError('commands'))
+44 -12
View File
@@ -5,7 +5,7 @@ import Debug from 'debug'
import getPort from 'get-port'
import path from 'path'
import urlUtil from 'url'
import { launch } from '@packages/launcher/lib/browsers'
import { launch, LaunchedBrowser } from '@packages/launcher/lib/browsers'
import FirefoxProfile from 'firefox-profile'
import firefoxUtil from './firefox-util'
import utils from './utils'
@@ -16,6 +16,8 @@ import os from 'os'
import treeKill from 'tree-kill'
import mimeDb from 'mime-db'
import { getRemoteDebuggingPort } from './protocol'
import type { BrowserCriClient } from './browser-cri-client'
import type { Automation } from '../automation'
const errors = require('../errors')
@@ -341,13 +343,21 @@ toolbar {
`
export function _createDetachedInstance (browserInstance: BrowserInstance): BrowserInstance {
let browserCriClient
export function _createDetachedInstance (browserInstance: BrowserInstance, browserCriClient?: BrowserCriClient): BrowserInstance {
const detachedInstance: BrowserInstance = new EventEmitter() as BrowserInstance
detachedInstance.pid = browserInstance.pid
// kill the entire process tree, from the spawned instance up
detachedInstance.kill = (): void => {
// Close browser cri client socket. Do nothing on failure here since we're shutting down anyway
if (browserCriClient) {
browserCriClient.close().catch()
browserCriClient = undefined
}
treeKill(browserInstance.pid, (err?, result?) => {
debug('force-exit of process tree complete %o', { err, result })
detachedInstance.emit('exit')
@@ -357,8 +367,12 @@ export function _createDetachedInstance (browserInstance: BrowserInstance): Brow
return detachedInstance
}
export async function connectToNewSpec (browser: Browser, options: any = {}, automation: Automation) {
await firefoxUtil.connectToNewSpec(options, automation, browserCriClient)
}
export function connectToExisting () {
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for browser')
throw new Error('Attempting to connect to existing browser for Cypress in Cypress which is not yet implemented for firefox')
}
export async function open (browser: Browser, url, options: any = {}, automation): Promise<BrowserInstance> {
@@ -506,24 +520,42 @@ export async function open (browser: Browser, url, options: any = {}, automation
debug('launch in firefox', { url, args: launchOptions.args })
const browserInstance = await launch(browser, 'about:blank', launchOptions.args, {
const browserInstance = await launch(browser, 'about:blank', remotePort, launchOptions.args, {
// sets headless resolution to 1280x720 by default
// user can overwrite this default with these env vars or --height, --width arguments
MOZ_HEADLESS_WIDTH: '1280',
MOZ_HEADLESS_HEIGHT: '721',
})
}) as LaunchedBrowser & { browserCriClient: BrowserCriClient}
try {
await firefoxUtil.setup({ automation, extensions: launchOptions.extensions, url, foxdriverPort, marionettePort, remotePort, onError: options.onError })
browserCriClient = await firefoxUtil.setup({ automation, extensions: launchOptions.extensions, url, foxdriverPort, marionettePort, remotePort, onError: options.onError })
if (os.platform() === 'win32') {
// override the .kill method for Windows so that the detached Firefox process closes between specs
// @see https://github.com/cypress-io/cypress/issues/6392
return _createDetachedInstance(browserInstance, browserCriClient)
}
// monkey-patch the .kill method to that the CDP connection is closed
const originalBrowserKill = browserInstance.kill
/* @ts-expect-error */
browserInstance.kill = (...args) => {
debug('closing remote interface client')
// Do nothing on failure here since we're shutting down anyway
if (browserCriClient) {
browserCriClient.close().catch()
browserCriClient = undefined
}
debug('closing firefox')
originalBrowserKill.apply(browserInstance, args)
}
} catch (err) {
errors.throw('FIREFOX_COULD_NOT_CONNECT', err)
}
if (os.platform() === 'win32') {
// override the .kill method for Windows so that the detached Firefox process closes between specs
// @see https://github.com/cypress-io/cypress/issues/6392
return _createDetachedInstance(browserInstance)
}
return browserInstance
}
+12 -1
View File
@@ -114,7 +114,7 @@ module.exports = {
return utils.getBrowsers()
},
connectToExisting (browser, options = {}, automation) {
async connectToExisting (browser, options = {}, automation) {
const browserLauncher = getBrowserLauncher(browser)
if (!browserLauncher) {
@@ -124,6 +124,17 @@ module.exports = {
return browserLauncher.connectToExisting(browser, options, automation)
},
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
return browserLauncher.connectToNewSpec(browser, options, automation)
},
open (browser, options = {}, automation, ctx) {
return kill(true)
.then(() => {
+2 -96
View File
@@ -1,15 +1,8 @@
import _ from 'lodash'
import CRI from 'chrome-remote-interface'
import { connect } from '@packages/network'
import Bluebird from 'bluebird'
import la from 'lazy-ass'
import Debug from 'debug'
import type { Socket } from 'net'
import utils from './utils'
const errors = require('../errors')
const is = require('check-more-types')
const debug = Debug('cypress:server:browsers:protocol')
export function _getDelayMsForRetry (i, browserName) {
if (i < 10) {
@@ -39,95 +32,8 @@ export function _connectAsync (opts) {
})
}
/**
* Tries to find the starting page (probably blank tab)
* among all targets returned by CRI.List call.
*
* @returns {string} web socket debugger url
*/
const findStartPage = (targets, url = 'about:blank') => {
debug('CRI List %o', { numTargets: targets.length, targets, url })
// activate the first available id
// find the first target page that's a real tab
// and not the dev tools or background page.
// since we open a blank page first, it has a special url
const newTabTargetFields = {
type: 'page',
url,
}
const target = _.find(targets, newTabTargetFields)
la(target, 'could not find CRI target')
debug('found CRI target %o', target)
return target.webSocketDebuggerUrl
}
const findStartPageTarget = (connectOpts, url) => {
debug('CRI.List %o', connectOpts)
// what happens if the next call throws an error?
// it seems to leave the browser instance open
// need to clone connectOpts, CRI modifies it
return CRI.List(_.clone(connectOpts)).then((targets) => findStartPage(targets, url))
}
export async function getRemoteDebuggingPort () {
const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) || utils.getPort()
const port = Number(process.env.CYPRESS_REMOTE_DEBUGGING_PORT) || await utils.getPort()
return port || utils.getPort()
}
/**
* Waits for the port to respond with connection to Chrome Remote Interface
* @param {number} port Port number to connect to
* @param {string} browserName Browser name, for warning/error messages
*/
export const getWsTargetFor = (port: number, browserName: string, url?: string | null) => {
debug('Getting WS connection to CRI on port %d', port)
la(is.port(port), 'expected port number', port)
let retryIndex = 0
// force ipv4
// https://github.com/cypress-io/cypress/issues/5912
const connectOpts = {
host: '127.0.0.1',
port,
getDelayMsForRetry: (i) => {
retryIndex = i
return _getDelayMsForRetry(i, browserName)
},
}
return _connectAsync(connectOpts)
.then(() => {
const retry = () => {
debug('attempting to find CRI target... %o', { retryIndex })
return findStartPageTarget(connectOpts, url)
.catch((err) => {
retryIndex++
const delay = _getDelayMsForRetry(retryIndex, browserName)
debug('error finding CRI target, maybe retrying %o', { delay, err })
if (typeof delay === 'undefined') {
throw err
}
return Bluebird.delay(delay)
.then(retry)
})
}
return retry()
})
.catch((err) => {
debug('failed to connect to CDP %o', { connectOpts, err })
errors.throw('CDP_COULD_NOT_CONNECT', port, err, browserName)
})
return port
}
+18 -25
View File
@@ -957,7 +957,7 @@ module.exports = {
},
launchBrowser (options = {}) {
const { browser, spec, writeVideoFrame, setScreenshotMetadata, project, screenshots, projectRoot, onError } = options
const { browser, spec, writeVideoFrame, setScreenshotMetadata, project, screenshots, projectRoot, shouldLaunchNewTab, onError } = options
const browserOpts = getDefaultBrowserOptsByFamily(browser, project, writeVideoFrame, onError)
@@ -988,6 +988,7 @@ module.exports = {
const warnings = {}
browserOpts.projectRoot = projectRoot
browserOpts.shouldLaunchNewTab = shouldLaunchNewTab
browserOpts.onWarning = (err) => {
const { message } = err
@@ -1009,12 +1010,6 @@ module.exports = {
return openProject.launch(browser, spec, browserOpts)
},
navigateToNextSpec (spec) {
debug('navigating to next spec')
return openProject.changeUrlToSpec(spec)
},
listenForProjectEnd (project, exit) {
return new Promise((resolve, reject) => {
if (exit === false) {
@@ -1086,7 +1081,7 @@ module.exports = {
return this.currentWriteVideoFrameCallback(...arguments)
},
waitForBrowserToConnect (options = {}, shouldLaunchBrowser = true) {
waitForBrowserToConnect (options = {}) {
const { project, socketId, timeout, onError, writeVideoFrame, spec } = options
const browserTimeout = process.env.CYPRESS_INTERNAL_BROWSER_CONNECT_TIMEOUT || timeout || 60000
let attempts = 0
@@ -1114,13 +1109,6 @@ module.exports = {
const wait = () => {
debug('waiting for socket to connect and browser to launch...')
if (!shouldLaunchBrowser) {
// If we do not launch the browser,
// we tell it that we are ready
// to receive the next spec
return Promise.resolve(this.navigateToNextSpec(options.spec))
}
return Promise.join(
this.waitForSocketConnection(project, socketId)
.tap(() => {
@@ -1181,7 +1169,7 @@ module.exports = {
},
waitForTestsToFinishRunning (options = {}) {
const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet, config, testingType } = options
const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet, config } = options
// https://github.com/cypress-io/cypress/issues/2370
// delay 1 second if we're recording a video to give
@@ -1257,13 +1245,17 @@ module.exports = {
}
}
if (testingType === 'e2e') {
// always close the browser now as opposed to letting
// it exit naturally with the parent process due to
// electron bug in windows
debug('attempting to close the browser')
await openProject.closeBrowser()
}
// Close the browser if the environment variable is set to do so
// if (process.env.CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH) {
// debug('attempting to close the browser')
// await openProject.closeBrowser()
// } else {
debug('attempting to close the browser tab')
await openProject.closeBrowserTabs()
// }
debug('resetting server state')
openProject.projectBase.server.reset()
if (videoExists && !skippedSpec && endVideoCapture && !videoCaptureFailed) {
const ffmpegChaptersConfig = videoCapture.generateFfmpegChaptersConfig(results.tests)
@@ -1490,7 +1482,7 @@ module.exports = {
videoCompression: options.videoCompression,
videoUploadOnPasses: options.videoUploadOnPasses,
quiet: options.quiet,
testingType: options.testingType,
browser,
}),
connection: this.waitForBrowserToConnect({
@@ -1503,8 +1495,9 @@ module.exports = {
socketId: options.socketId,
webSecurity: options.webSecurity,
projectRoot: options.projectRoot,
shouldLaunchNewTab: !firstSpec, // !process.env.CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH && !firstSpec,
// TODO(tim): investigate the socket disconnect
}, process.env.CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH || options.testingType === 'e2e' || firstSpec),
}),
})
})
},
+19 -14
View File
@@ -43,20 +43,6 @@ export class OpenProject {
return this.projectBase
}
changeUrlToSpec (spec: Cypress.Cypress['spec']) {
if (!this.projectBase) {
return
}
const newSpecUrl = getSpecUrl({
spec,
browserUrl: this.projectBase.cfg.browserUrl,
projectRoot: this.projectBase.projectRoot,
})
this.projectBase.changeToUrl(newSpecUrl)
}
async launch (browser, spec: Cypress.Cypress['spec'], options: LaunchOpts = {
onError: () => undefined,
}) {
@@ -183,6 +169,17 @@ export class OpenProject {
return browsers.connectToExisting(browser, options, automation)
}
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 browsers.connectToNewSpec(browser, { onInitializeNewBrowserTab, ...options }, automation)
}
return browsers.open(browser, options, automation, this._ctx)
})
}
@@ -194,6 +191,14 @@ export class OpenProject {
return browsers.close()
}
async closeBrowserTabs () {
return this.projectBase?.closeBrowserTabs()
}
async resetBrowserState () {
return this.projectBase?.resetBrowserState()
}
closeOpenProjectAndBrowsers () {
this.projectBase?.close().catch((e) => {
this._ctx?.logTraceError(e)
+8
View File
@@ -474,6 +474,14 @@ export class ProjectBase<TServer extends Server> extends EE {
this.server.changeToUrl(url)
}
async closeBrowserTabs () {
return this.server.socket.closeBrowserTabs()
}
async resetBrowserState () {
return this.server.socket.resetBrowserState()
}
async sendFocusBrowserMessage () {
if (this.browser.family === 'firefox') {
await browsers.setFocus()
+22
View File
@@ -74,6 +74,8 @@ const retry = (fn: (res: any) => void) => {
}
export class SocketBase {
private _sendCloseBrowserTabsMessage
private _sendResetBrowserStateMessage
private _sendFocusBrowserMessage
protected ended: boolean
@@ -275,6 +277,14 @@ export class SocketBase {
})
})
this._sendCloseBrowserTabsMessage = async () => {
await automationRequest('close:browser:tabs', {})
}
this._sendResetBrowserStateMessage = async () => {
await automationRequest('reset:browser:state', {})
}
this._sendFocusBrowserMessage = async () => {
await automationRequest('focus:browser:window', {})
}
@@ -544,6 +554,18 @@ export class SocketBase {
return this.toRunner('change:to:url', url)
}
async closeBrowserTabs () {
if (this._sendCloseBrowserTabsMessage) {
await this._sendCloseBrowserTabsMessage()
}
}
async resetBrowserState () {
if (this._sendResetBrowserStateMessage) {
await this._sendResetBrowserStateMessage()
}
}
async sendFocusBrowserMessage () {
await this._sendFocusBrowserMessage()
}
+2 -1
View File
@@ -40,7 +40,7 @@
"chalk": "2.4.2",
"check-more-types": "2.24.0",
"chokidar": "3.5.1",
"chrome-remote-interface": "0.28.2",
"chrome-remote-interface": "0.31.1",
"cli-table3": "0.5.1",
"coffeescript": "1.12.7",
"color-string": "1.5.5",
@@ -150,6 +150,7 @@
"@tooling/system-tests": "0.0.0-development",
"@types/chai-as-promised": "7.1.2",
"@types/chrome": "0.0.101",
"@types/chrome-remote-interface": "0.31.4",
"@types/http-proxy": "1.17.4",
"@types/node": "14.14.31",
"awesome-typescript-loader": "5.2.1",
@@ -46,6 +46,7 @@ const appData = require(`../../lib/util/app_data`)
const electronApp = require('../../lib/util/electron-app')
const savedState = require(`../../lib/saved_state`)
const { getCtx } = require(`../../lib/makeDataContext`)
const { BrowserCriClient } = require(`../../lib/browsers/browser-cri-client`)
const TYPICAL_BROWSERS = [
{
@@ -290,6 +291,7 @@ describe('lib/cypress', () => {
sinon.stub(runMode, 'waitForSocketConnection').resolves()
sinon.stub(runMode, 'listenForProjectEnd').resolves({ stats: { failures: 0 } })
sinon.stub(browsers, 'open')
sinon.stub(browsers, 'connectToNewSpec')
sinon.stub(commitInfo, 'getRemoteOrigin').resolves('remoteOrigin')
})
@@ -408,7 +410,7 @@ describe('lib/cypress', () => {
})
it('runs project by limiting spec files via config.testFiles string glob pattern', function () {
return cypress.start([`--run-project=${this.todosPath}`, `--config=testFiles=${this.todosPath}/tests/test2.coffee`])
return cypress.start([`--run-project=${this.todosPath}`, `--config={"e2e":{"specPattern":"${this.todosPath}/tests/test2.coffee"}}`])
.then(() => {
expect(browsers.open).to.be.calledWithMatch(ELECTRON_BROWSER, { url: 'http://localhost:8888/__/#/specs/runner?file=tests/test2.coffee' })
this.expectExitWith(0)
@@ -416,11 +418,11 @@ describe('lib/cypress', () => {
})
it('runs project by limiting spec files via config.testFiles as a JSON array of string glob patterns', function () {
return cypress.start([`--run-project=${this.todosPath}`, '--config=testFiles=["**/test2.coffee","**/test1.js"]'])
return cypress.start([`--run-project=${this.todosPath}`, '--config={"e2e":{"specPattern":["**/test2.coffee","**/test1.js"]}}'])
.then(() => {
expect(browsers.open).to.be.calledWithMatch(ELECTRON_BROWSER, { url: 'http://localhost:8888/__/#/specs/runner?file=tests/test2.coffee' })
}).then(() => {
expect(browsers.open).to.be.calledWithMatch(ELECTRON_BROWSER, { url: 'http://localhost:8888/__/#/specs/runner?file=tests/test1.js' })
expect(browsers.connectToNewSpec).to.be.calledWithMatch(ELECTRON_BROWSER, { url: 'http://localhost:8888/__/#/specs/runner?file=tests/test1.js' })
this.expectExitWith(0)
})
})
@@ -975,15 +977,18 @@ describe('lib/cypress', () => {
// during testing, do not try to connect to the remote interface or
// use the Chrome remote interface client
const criClient = {
ensureMinimumProtocolVersion: sinon.stub().resolves(),
close: sinon.stub().resolves(),
on: sinon.stub(),
send: sinon.stub(),
}
const browserCriClient = {
ensureMinimumProtocolVersion: sinon.stub().resolves(),
attachToTargetUrl: sinon.stub().resolves(criClient),
close: sinon.stub().resolves(),
}
sinon.stub(chromeBrowser, '_writeExtension').resolves()
sinon.stub(chromeBrowser, '_connectToChromeRemoteInterface').resolves(criClient)
sinon.stub(BrowserCriClient, 'create').resolves(browserCriClient)
// the "returns(resolves)" stub is due to curried method
// it accepts URL to visit and then waits for actual CRI client reference
// and only then navigates to that URL
@@ -1008,7 +1013,7 @@ describe('lib/cypress', () => {
expect(args[0], 'found and used Chrome').to.deep.eq(launchedChrome)
const browserArgs = args[2]
const browserArgs = args[3]
expect(browserArgs.slice(0, 4), 'first 4 custom launch arguments to Chrome').to.deep.eq([
'chrome', 'foo', 'bar', 'baz',
@@ -1019,7 +1024,8 @@ describe('lib/cypress', () => {
expect(chromeBrowser._navigateUsingCRI).to.have.been.calledOnce
expect(chromeBrowser._setAutomation).to.have.been.calledOnce
expect(chromeBrowser._connectToChromeRemoteInterface).to.have.been.calledOnce
expect(BrowserCriClient.create).to.have.been.calledOnce
expect(browserCriClient.attachToTargetUrl).to.have.been.calledOnce
})
})
@@ -0,0 +1,200 @@
import { BrowserCriClient } from '../../../lib/browsers/browser-cri-client'
import * as CriClient from '../../../lib/browsers/cri-client'
import { expect, proxyquire, sinon } from '../../spec_helper'
import * as protocol from '../../../lib/browsers/protocol'
const HOST = '127.0.0.1'
const PORT = 50505
const THROWS_PORT = 66666
describe('lib/browsers/cri-client', function () {
let browserCriClient: {
BrowserCriClient: {
create: typeof BrowserCriClient.create
}
}
let send: sinon.SinonStub
let close: sinon.SinonStub
let criClientCreateStub: sinon.SinonStub
let criImport: sinon.SinonStub & {
Version: sinon.SinonStub
}
let onError: sinon.SinonStub
let getClient: () => ReturnType<typeof BrowserCriClient.create>
beforeEach(function () {
sinon.stub(protocol, '_connectAsync')
criImport = sinon.stub()
criImport.Version = sinon.stub()
criImport.Version.withArgs({ host: HOST, port: PORT }).resolves({ webSocketDebuggerUrl: 'http://web/socket/url' })
criImport.Version.withArgs({ host: HOST, port: THROWS_PORT }).throws()
.onSecondCall().throws()
.onThirdCall().resolves({ webSocketDebuggerUrl: 'http://web/socket/url' })
send = sinon.stub()
close = sinon.stub()
criClientCreateStub = sinon.stub(CriClient, 'create').withArgs('http://web/socket/url', onError).resolves({
send,
close,
})
browserCriClient = proxyquire('../lib/browsers/browser-cri-client', {
'chrome-remote-interface': criImport,
})
getClient = () => browserCriClient.BrowserCriClient.create(PORT, 'Chrome', onError)
})
context('.create', function () {
it('returns an instance of the Browser CRI client', async function () {
const client = await getClient()
expect(client.attachToNewUrl).to.be.instanceOf(Function)
})
it('throws an error when _connectAsync fails', function () {
(protocol._connectAsync as any).restore()
sinon.stub(protocol, '_connectAsync').throws()
expect(getClient()).to.be.rejected
})
it('retries when Version fails', async function () {
sinon.stub(protocol, '_getDelayMsForRetry')
.onFirstCall().returns(100)
.onSecondCall().returns(100)
.onThirdCall().returns(100)
const client = await browserCriClient.BrowserCriClient.create(THROWS_PORT, 'Chrome', onError)
expect(client.attachToNewUrl).to.be.instanceOf(Function)
expect(criImport.Version).to.be.calledThrice
})
it('throws when Version fails more than allowed', async function () {
sinon.stub(protocol, '_getDelayMsForRetry')
.onFirstCall().returns(500)
.onSecondCall().returns(undefined)
expect(browserCriClient.BrowserCriClient.create(THROWS_PORT, 'Chrome', onError)).to.be.rejected
})
context('#ensureMinimumProtocolVersion', function () {
function withProtocolVersion (actual, test) {
return getClient()
.then((client: any) => {
client.versionInfo = { 'Protocol-Version': actual }
return client.ensureMinimumProtocolVersion(test)
})
}
it('resolves if protocolVersion = current', function () {
return expect(withProtocolVersion('1.3', '1.3')).to.be.fulfilled
})
it('resolves if protocolVersion > current', function () {
return expect(withProtocolVersion('1.4', '1.3')).to.be.fulfilled
})
it('rejects if protocolVersion < current', function () {
return expect(withProtocolVersion('1.2', '1.3')).to.be
.rejectedWith('A minimum CDP version of v1.3 is required, but the current browser has v1.2.')
})
})
context('#attachToTargetUrl', function () {
it('creates a page client when the passed in url is found', async function () {
const mockPageClient = {}
send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] })
criClientCreateStub.withArgs('1', onError, HOST, PORT).resolves(mockPageClient)
const browserClient = await getClient()
const client = await browserClient.attachToTargetUrl('http://foo.com')
expect(client).to.be.equal(mockPageClient)
})
it('throws when the passed in url is not found', async function () {
const mockPageClient = {}
send.withArgs('Target.getTargets').resolves({ targetInfos: [{ targetId: '1', url: 'http://foo.com' }, { targetId: '2', url: 'http://bar.com' }] })
criClientCreateStub.withArgs('1', onError).resolves(mockPageClient)
const browserClient = await getClient()
expect(browserClient.attachToTargetUrl('http://baz.com')).to.be.rejected
})
})
context('#attachToNewUrl', function () {
it('creates new target and creates a page client with the passed in url', async function () {
const mockPageClient = {}
send.withArgs('Target.createTarget', { url: 'http://foo.com' }).resolves({ targetId: '10' })
criClientCreateStub.withArgs('10', onError, HOST, PORT).resolves(mockPageClient)
const browserClient = await getClient()
const client = await browserClient.attachToNewUrl('http://foo.com')
expect(client).to.be.equal(mockPageClient)
})
})
context('#closeCurrentTarget', function () {
it('closes the currently attached target', async function () {
const mockCurrentlyAttachedTarget = {
targetId: '100',
close: sinon.stub().resolves(sinon.stub().resolves()),
}
send.withArgs('Target.closeTarget', { targetId: '100' }).resolves()
const browserClient = await getClient() as any
browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget
await browserClient.closeCurrentTarget()
expect(mockCurrentlyAttachedTarget.close).to.be.called
})
it('throws when there is no currently attached target', async function () {
const browserClient = await getClient() as any
expect(browserClient.closeCurrentTarget()).to.be.rejected
})
})
context('#close', function () {
it('closes the currently attached target if it exists and the browser client', async function () {
const mockCurrentlyAttachedTarget = {
close: sinon.stub().resolves(),
}
const browserClient = await getClient() as any
browserClient.currentlyAttachedTarget = mockCurrentlyAttachedTarget
await browserClient.close()
expect(mockCurrentlyAttachedTarget.close).to.be.called
expect(close).to.be.called
})
it('just the browser client with no currently attached target', async function () {
const browserClient = await getClient() as any
await browserClient.close()
expect(close).to.be.called
})
})
})
})
@@ -101,6 +101,26 @@ describe('lib/browsers/index', () => {
})
})
context('.connectToNewSpec', () => {
it('throws an error if browser family doesn\'t exist', () => {
return browsers.connectToNewSpec({
name: 'foo-bad-bang',
family: 'foo-bad',
}, {
browsers: [],
})
.then((e) => {
throw new Error('should\'ve failed')
}).catch((err) => {
// by being explicit with assertions, if something is unexpected
// we will get good error message that includes the "err" object
expect(err).to.have.property('type').to.eq('BROWSER_NOT_FOUND_BY_NAME')
expect(err).to.have.property('message').to.contain('The specified browser was not found on your system or is not supported by Cypress: `foo-bad-bang`')
})
})
})
context('.open', () => {
it('throws an error if browser family doesn\'t exist', () => {
return browsers.open({
@@ -67,11 +67,12 @@ context('lib/browsers/cdp_automation', () => {
})
context('.CdpAutomation', () => {
beforeEach(function () {
beforeEach(async function () {
this.sendDebuggerCommand = sinon.stub()
this.onFn = sinon.stub()
this.sendCloseTargetCommand = sinon.stub()
this.automation = new CdpAutomation(this.sendDebuggerCommand, this.onFn)
this.automation = await CdpAutomation.create(this.sendDebuggerCommand, this.onFn, this.sendCloseTargetCommand, null)
this.sendDebuggerCommand
.throws(new Error('not stubbed'))
@@ -219,6 +220,28 @@ context('lib/browsers/cdp_automation', () => {
})
})
describe('reset:browser:state', function () {
it('sends Storage.clearDataForOrigin and Network.clearBrowserCache', async function () {
this.sendDebuggerCommand.withArgs('Storage.clearDataForOrigin', { origin: '*', storageTypes: 'all' }).resolves()
this.sendDebuggerCommand.withArgs('Network.clearBrowserCache').resolves()
await this.onRequest('reset:browser:state')
expect(this.sendDebuggerCommand).to.be.calledWith('Storage.clearDataForOrigin', { origin: '*', storageTypes: 'all' })
expect(this.sendDebuggerCommand).to.be.calledWith('Network.clearBrowserCache')
})
})
describe('close:browser:tabs', function () {
it('sends the close target message for the attached target tabs', async function () {
this.sendCloseTargetCommand.resolves()
await this.onRequest('close:browser:tabs')
expect(this.sendCloseTargetCommand).to.be.called
})
})
describe('focus:browser:window', function () {
it('sends Page.bringToFront when focus is requested', function () {
this.sendDebuggerCommand.withArgs('Page.bringToFront').resolves()
@@ -11,13 +11,13 @@ const plugins = require(`../../../lib/plugins`)
const utils = require(`../../../lib/browsers/utils`)
const chrome = require(`../../../lib/browsers/chrome`)
const { fs } = require(`../../../lib/util/fs`)
const { BrowserCriClient } = require('../../../lib/browsers/browser-cri-client')
describe('lib/browsers/chrome', () => {
context('#open', () => {
beforeEach(function () {
// mock CRI client during testing
this.criClient = {
ensureMinimumProtocolVersion: sinon.stub().resolves(),
this.pageCriClient = {
send: sinon.stub().resolves(),
Page: {
screencastFrame: sinon.stub().returns(),
@@ -26,6 +26,12 @@ describe('lib/browsers/chrome', () => {
on: sinon.stub(),
}
this.browserCriClient = {
attachToTargetUrl: sinon.stub().resolves(this.pageCriClient),
close: sinon.stub().resolves(),
ensureMinimumProtocolVersion: sinon.stub().withArgs('1.3').resolves(),
}
this.automation = {
push: sinon.stub(),
use: sinon.stub().returns(),
@@ -37,16 +43,16 @@ describe('lib/browsers/chrome', () => {
}
this.onCriEvent = (event, data, options) => {
this.criClient.on.withArgs(event).yieldsAsync(data)
this.pageCriClient.on.withArgs(event).yieldsAsync(data)
return chrome.open('chrome', 'http://', options, this.automation)
return chrome.open({ isHeadless: true }, 'http://', options, this.automation)
.then(() => {
this.criClient.on = undefined
this.pageCriClient.on = undefined
})
}
sinon.stub(chrome, '_writeExtension').resolves('/path/to/ext')
sinon.stub(chrome, '_connectToChromeRemoteInterface').resolves(this.criClient)
sinon.stub(BrowserCriClient, 'create').resolves(this.browserCriClient)
sinon.stub(plugins, 'execute').callThrough()
sinon.stub(launch, 'launch').resolves(this.launchedBrowser)
sinon.stub(utils, 'getProfileDir').returns('/profile/dir')
@@ -63,25 +69,25 @@ describe('lib/browsers/chrome', () => {
afterEach(function () {
mockfs.restore()
expect(this.criClient.ensureMinimumProtocolVersion).to.be.calledOnce
expect(this.browserCriClient.ensureMinimumProtocolVersion).to.be.calledOnce
})
it('focuses on the page, calls CRI Page.visit, enables Page events, and sets download behavior', function () {
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
expect(utils.getPort).to.have.been.calledOnce // to get remote interface port
expect(this.criClient.send.callCount).to.equal(5)
expect(this.criClient.send).to.have.been.calledWith('Page.bringToFront')
expect(this.pageCriClient.send.callCount).to.equal(5)
expect(this.pageCriClient.send).to.have.been.calledWith('Page.bringToFront')
expect(this.criClient.send).to.have.been.calledWith('Page.navigate')
expect(this.criClient.send).to.have.been.calledWith('Page.enable')
expect(this.criClient.send).to.have.been.calledWith('Page.setDownloadBehavior')
expect(this.criClient.send).to.have.been.calledWith('Network.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Page.navigate')
expect(this.pageCriClient.send).to.have.been.calledWith('Page.enable')
expect(this.pageCriClient.send).to.have.been.calledWith('Page.setDownloadBehavior')
expect(this.pageCriClient.send).to.have.been.calledWith('Network.enable')
})
})
it('is noop without before:browser:launch', function () {
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
expect(plugins.execute).not.to.be.called
})
@@ -95,20 +101,20 @@ describe('lib/browsers/chrome', () => {
plugins.execute.resolves(null)
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
// to initialize remote interface client and prepare for true tests
// we load the browser with blank page first
expect(launch.launch).to.be.calledWith('chrome', 'about:blank', args)
expect(launch.launch).to.be.calledWith({ isHeadless: true }, 'about:blank', 50505, args)
})
})
it('sets default window size and DPR in headless mode', function () {
chrome._writeExtension.restore()
return chrome.open({ isHeadless: true, isHeaded: false }, 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.include.members([
'--headless',
@@ -121,9 +127,9 @@ describe('lib/browsers/chrome', () => {
it('does not load extension in headless mode', function () {
chrome._writeExtension.restore()
return chrome.open({ isHeadless: true, isHeaded: false }, 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.include.members([
'--headless',
@@ -154,7 +160,7 @@ describe('lib/browsers/chrome', () => {
channel: 'stable',
}, 'http://', {}, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.include.members([
`--user-data-dir=${fullPath}`,
@@ -171,9 +177,9 @@ describe('lib/browsers/chrome', () => {
const onWarning = sinon.stub()
return chrome.open('chrome', 'http://', { onWarning }, this.automation)
return chrome.open({ isHeaded: true }, 'http://', { onWarning }, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.deep.eq([
'--foo=bar',
@@ -195,9 +201,9 @@ describe('lib/browsers/chrome', () => {
const pathToTheme = extension.getPathToTheme()
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeaded: true }, 'http://', {}, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.include.members([
'--foo=bar',
@@ -217,9 +223,9 @@ describe('lib/browsers/chrome', () => {
const onWarning = sinon.stub()
return chrome.open('chrome', 'http://', { onWarning }, this.automation)
return chrome.open({ isHeaded: true }, 'http://', { onWarning }, this.automation)
.then(() => {
const args = launch.launch.firstCall.args[2]
const args = launch.launch.firstCall.args[3]
expect(args).to.include.members([
'--foo=bar',
@@ -279,7 +285,7 @@ describe('lib/browsers/chrome', () => {
sinon.stub(fs, 'outputJson').resolves()
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
expect(fs.outputJson).to.be.calledWith('/profile/dir/Default/Preferences', {
profile: {
@@ -296,21 +302,21 @@ describe('lib/browsers/chrome', () => {
kill,
} = this.launchedBrowser
return chrome.open('chrome', 'http://', {}, this.automation)
return chrome.open({ isHeadless: true }, 'http://', {}, this.automation)
.then(() => {
expect(typeof this.launchedBrowser.kill).to.eq('function')
this.launchedBrowser.kill()
expect(this.criClient.close).to.be.calledOnce
expect(this.browserCriClient.close).to.be.calledOnce
expect(kill).to.be.calledOnce
})
})
it('rejects if CDP version check fails', function () {
this.criClient.ensureMinimumProtocolVersion.rejects()
this.browserCriClient.ensureMinimumProtocolVersion.throws()
return expect(chrome.open('chrome', 'http://', {}, this.automation)).to.be.rejectedWith('Cypress requires at least Chrome 64.')
return expect(chrome.open({ isHeadless: true }, 'http://', {}, this.automation)).to.be.rejectedWith('Cypress requires at least Chrome 64.')
})
// https://github.com/cypress-io/cypress/issues/9265
@@ -321,9 +327,9 @@ describe('lib/browsers/chrome', () => {
return this.onCriEvent('Page.screencastFrame', frameMeta, options)
.then(() => {
expect(this.criClient.send).to.have.been.calledWith('Page.startScreencast')
expect(this.pageCriClient.send).to.have.been.calledWith('Page.startScreencast')
expect(write).to.have.been.calledWith(frameMeta)
expect(this.criClient.send).to.have.been.calledWith('Page.screencastFrameAck', { sessionId: frameMeta.sessionId })
expect(this.pageCriClient.send).to.have.been.calledWith('Page.screencastFrameAck', { sessionId: frameMeta.sessionId })
})
})
@@ -364,6 +370,47 @@ describe('lib/browsers/chrome', () => {
})
})
context('#connectToNewSpec', () => {
it('launches a new tab, connects a cri client to it, starts video, navigates to the spec url, and handles downloads', async function () {
const pageCriClient = {
send: sinon.stub().resolves(),
on: sinon.stub(),
}
const browserCriClient = {
attachToNewUrl: sinon.stub().withArgs('about:blank').resolves(pageCriClient),
}
const automation = {
use: sinon.stub().returns(),
}
const launchedBrowser = {
kill: sinon.stub().returns(),
}
let onInitializeNewBrowserTabCalled = false
const options = { onError: () => {}, url: 'https://www.google.com', downloadsFolder: '/tmp/folder', onInitializeNewBrowserTab: () => {
onInitializeNewBrowserTabCalled = true
} }
sinon.stub(chrome, '_getBrowserCriClient').returns(browserCriClient)
sinon.stub(chrome, '_maybeRecordVideo').withArgs(pageCriClient, options, 354).resolves()
sinon.stub(chrome, '_navigateUsingCRI').withArgs(pageCriClient, options.url, 354).resolves()
sinon.stub(chrome, '_handleDownloads').withArgs(pageCriClient, options.downloadFolder, automation).resolves()
await chrome.connectToNewSpec({ majorVersion: 354 }, options, automation, launchedBrowser)
expect(browserCriClient.attachToNewUrl).to.be.called
expect(automation.use).to.be.called
expect(chrome._getBrowserCriClient).to.be.called
expect(chrome._maybeRecordVideo).to.be.called
expect(chrome._navigateUsingCRI).to.be.called
expect(chrome._handleDownloads).to.be.called
expect(onInitializeNewBrowserTabCalled).to.be.true
})
})
context('#_getArgs', () => {
it('disables gpu when linux', () => {
sinon.stub(os, 'platform').returns('linux')
@@ -5,13 +5,17 @@ import EventEmitter from 'events'
const { expect, proxyquire, sinon } = require('../../spec_helper')
const DEBUGGER_URL = 'http://foo'
const HOST = '127.0.0.1'
const PORT = 50505
describe('lib/browsers/cri-client', function () {
let criClient: {
create: typeof create
}
let send: sinon.SinonStub
let criImport: sinon.SinonStub
let criImport: sinon.SinonStub & {
New: sinon.SinonStub
}
let onError: sinon.SinonStub
let getClient: () => ReturnType<typeof create>
@@ -32,6 +36,8 @@ describe('lib/browsers/cri-client', function () {
_notifier: new EventEmitter(),
})
criImport.New = sinon.stub().withArgs({ host: HOST, port: PORT, url: 'about:blank' }).resolves({ webSocketDebuggerUrl: 'http://web/socket/url' })
criClient = proxyquire('../lib/browsers/cri-client', {
'chrome-remote-interface': criImport,
})
@@ -86,40 +92,5 @@ describe('lib/browsers/cri-client', function () {
})
})
})
context('#ensureMinimumProtocolVersion', function () {
function withProtocolVersion (actual, test) {
if (actual) {
send.withArgs('Browser.getVersion')
.resolves({ protocolVersion: actual })
}
return getClient()
.then((client) => {
return client.ensureMinimumProtocolVersion(test)
})
}
it('resolves if protocolVersion = current', function () {
return expect(withProtocolVersion('1.3', '1.3')).to.be.fulfilled
})
it('resolves if protocolVersion > current', function () {
return expect(withProtocolVersion('1.4', '1.3')).to.be.fulfilled
})
it('rejects if Browser.getVersion not supported yet', function () {
send.withArgs('Browser.getVersion')
.rejects()
return expect(withProtocolVersion(null, '1.3')).to.be
.rejectedWith('A minimum CDP version of v1.3 is required, but the current browser has an older version.')
})
it('rejects if protocolVersion < current', function () {
return expect(withProtocolVersion('1.2', '1.3')).to.be
.rejectedWith('A minimum CDP version of v1.3 is required, but the current browser has v1.2.')
})
})
})
})
@@ -70,6 +70,14 @@ 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)
expect(electron.open).to.be.called
})
})
context('.open', () => {
beforeEach(function () {
return this.stubForOpen()
@@ -290,6 +298,7 @@ describe('lib/browsers/electron', () => {
maximize: sinon.stub(),
setSize: sinon.stub(),
show: sinon.stub(),
destroy: sinon.stub(),
webContents: this.win.webContents,
}
@@ -355,6 +364,22 @@ describe('lib/browsers/electron', () => {
expect(this.newWin.show).to.be.called
})
})
it('registers onRequest automation middleware and calls destroy when requesting to close the browser tabs', function () {
sinon.spy(this.automation, 'use')
electron._render(this.url, this.automation, this.preferences, this.options)
.then(() => {
expect(Windows.create).to.be.calledWith(this.options.projectRoot, this.options)
expect(this.automation.use).to.be.called
expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function')
this.automation.use.lastCall.args[0].onRequest('close:browser:tabs')
expect(this.newWin.destroy).to.be.called
})
})
})
context('._defaultOptions', () => {
@@ -99,9 +99,34 @@ describe('lib/browsers/firefox', () => {
stubFoxdriver()
})
context('#connectToNewSpec', () => {
beforeEach(function () {
this.browser = { name: 'firefox', channel: 'stable' }
this.automation = {
use: sinon.stub().returns({}),
}
this.options = {
onError: () => {},
}
})
it('calls connectToNewSpec in firefoxUtil', function () {
sinon.stub(firefoxUtil, 'connectToNewSpec').withArgs(50505, this.options, this.automation).resolves()
firefox.connectToNewSpec(this.browser, 50505, this.options, this.automation)
expect(firefoxUtil.connectToNewSpec).to.be.called
})
})
context('#open', () => {
beforeEach(function () {
this.browser = { name: 'firefox', channel: 'stable' }
this.automation = {
use: sinon.stub().returns({}),
}
this.options = {
proxyUrl: 'http://proxy-url',
socketIoRoute: 'socket/io/route',
@@ -131,7 +156,7 @@ describe('lib/browsers/firefox', () => {
plugins.has.returns(true)
plugins.execute.resolves(null)
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(plugins.execute).to.be.called
})
})
@@ -139,7 +164,7 @@ describe('lib/browsers/firefox', () => {
it('does not execute before:browser:launch if not registered', function () {
plugins.has.returns(false)
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(plugins.execute).not.to.be.called
})
})
@@ -148,7 +173,7 @@ describe('lib/browsers/firefox', () => {
plugins.has.returns(true)
plugins.execute.resolves(null)
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('network.proxy.type', 1)
})
})
@@ -159,7 +184,7 @@ describe('lib/browsers/firefox', () => {
preferences: [],
})
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('network.proxy.type', 1)
})
})
@@ -170,7 +195,7 @@ describe('lib/browsers/firefox', () => {
preferences: { 'foo': 'bar' },
})
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('foo', 'bar')
})
})
@@ -181,7 +206,7 @@ describe('lib/browsers/firefox', () => {
extensions: ['/path/to/user/ext'],
})
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/ext' } })
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/user/ext' } })
@@ -194,7 +219,7 @@ describe('lib/browsers/firefox', () => {
extensions: 'not-an-array',
})
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(marionetteDriver.send).calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/ext' } })
expect(marionetteDriver.send).not.calledWithMatch({ name: 'Addon:Install', params: { path: '/path/to/user/ext' } })
@@ -204,13 +229,13 @@ describe('lib/browsers/firefox', () => {
it('sets user-agent preference if specified', function () {
this.options.userAgent = 'User Agent'
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('general.useragent.override', 'User Agent')
})
})
it('writes extension', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(utils.writeExtension).to.be.calledWith(this.options.browser, this.options.isTextTerminal, this.options.proxyUrl, this.options.socketIoRoute)
})
})
@@ -248,14 +273,14 @@ describe('lib/browsers/firefox', () => {
}, mockfs.getMockRoot())
}
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(getFile(`${process.env.HOME }/.config/Cypress/cy/test/browsers/firefox-stable/interactive/CypressExtension/background.js`).getMode()).to.be.equals(0o644)
})
})
// TODO: pick open port for debugger
it.skip('finds remote port for firefox debugger', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
// expect(firefoxUtil.findRemotePort).to.be.called
})
})
@@ -263,7 +288,7 @@ describe('lib/browsers/firefox', () => {
it('sets proxy-related preferences if specified', function () {
this.options.proxyServer = 'http://proxy-server:1234'
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('network.proxy.http', 'proxy-server')
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('network.proxy.ssl', 'proxy-server')
expect(FirefoxProfile.prototype.setPreference).to.be.calledWith('network.proxy.http_port', 1234)
@@ -274,7 +299,7 @@ describe('lib/browsers/firefox', () => {
})
it('does not set proxy-related preferences if not specified', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.setPreference).not.to.be.calledWith('network.proxy.http', 'proxy-server')
expect(FirefoxProfile.prototype.setPreference).not.to.be.calledWith('network.proxy.https', 'proxy-server')
expect(FirefoxProfile.prototype.setPreference).not.to.be.calledWith('network.proxy.http_port', 1234)
@@ -285,14 +310,14 @@ describe('lib/browsers/firefox', () => {
})
it('updates the preferences', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(FirefoxProfile.prototype.updatePreferences).to.be.called
})
})
it('launches with the url and args', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
expect(launch.launch).to.be.calledWith(this.browser, 'about:blank', [
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(launch.launch).to.be.calledWith(this.browser, 'about:blank', undefined, [
'-marionette',
'-new-instance',
'-foreground',
@@ -305,7 +330,7 @@ describe('lib/browsers/firefox', () => {
})
it('resolves the browser instance', function () {
return firefox.open(this.browser, 'http://', this.options).then((result) => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then((result) => {
expect(result).to.equal(this.browserInstance)
})
})
@@ -318,7 +343,7 @@ describe('lib/browsers/firefox', () => {
},
})
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
// @ts-ignore
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive')).containSubset({
'xulstore.json': '[foo xulstore.json]',
@@ -328,7 +353,7 @@ describe('lib/browsers/firefox', () => {
})
it('creates xulstore.json if not exist', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
// @ts-ignore
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive')).containSubset({
'xulstore.json': '{"chrome://browser/content/browser.xhtml":{"main-window":{"width":1280,"height":1024,"sizemode":"maximized"}}}\n',
@@ -337,7 +362,7 @@ describe('lib/browsers/firefox', () => {
})
it('creates chrome/userChrome.css if not exist', function () {
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive/chrome/userChrome.css')).ok
})
})
@@ -351,7 +376,7 @@ describe('lib/browsers/firefox', () => {
this.options.isTextTerminal = false
return firefox.open(this.browser, 'http://', this.options).then(() => {
return firefox.open(this.browser, 'http://', this.options, this.automation).then(() => {
// @ts-ignore
expect(specUtil.getFsPath('/path/to/appData/firefox-stable/interactive')).containSubset({
'CypressCache': {},
@@ -364,7 +389,7 @@ describe('lib/browsers/firefox', () => {
protocol._connectAsync.rejects()
await expect(firefox.open(this.browser, 'http://', this.options)).to.be.rejectedWith()
await expect(firefox.open(this.browser, 'http://', this.options, this.automation)).to.be.rejectedWith()
.then((wrapperErr) => {
expect(wrapperErr.message).to.include('Cypress failed to make a connection to Firefox.')
expect(wrapperErr.message).to.include(err.message)
@@ -373,7 +398,7 @@ describe('lib/browsers/firefox', () => {
context('returns BrowserInstance', function () {
it('from browsers.launch', async function () {
const instance = await firefox.open(this.browser, 'http://', this.options)
const instance = await firefox.open(this.browser, 'http://', this.options, this.automation)
expect(instance).to.eq(this.browserInstance)
})
@@ -381,7 +406,7 @@ describe('lib/browsers/firefox', () => {
// @see https://github.com/cypress-io/cypress/issues/6392
it('detached on Windows', async function () {
sinon.stub(os, 'platform').returns('win32')
const instance = await firefox.open(this.browser, 'http://', this.options)
const instance = await firefox.open(this.browser, 'http://', this.options, this.automation)
expect(instance).to.not.eq(this.browserInstance)
expect(instance.pid).to.eq(this.browserInstance.pid)
@@ -1,22 +1,15 @@
import '../../spec_helper'
import _ from 'lodash'
import Bluebird from 'bluebird'
import 'chai-as-promised' // for the types!
import chalk from 'chalk'
import 'chai-as-promised'
import { connect } from '@packages/network'
import CRI from 'chrome-remote-interface'
import { expect } from 'chai'
import humanInterval from 'human-interval'
import * as protocol from '../../../lib/browsers/protocol'
import sinon from 'sinon'
import snapshot from 'snap-shot-it'
import stripAnsi from 'strip-ansi'
import { stripIndents } from 'common-tags'
describe('lib/browsers/protocol', () => {
// protocol connects explicitly to this host
const host = '127.0.0.1'
context('._getDelayMsForRetry', () => {
it('retries as expected for up to 50 seconds', () => {
const log = sinon.spy(console, 'log')
@@ -42,146 +35,19 @@ describe('lib/browsers/protocol', () => {
})
})
context('.getWsTargetFor', () => {
const expectedCdpFailedError = stripIndents`
Cypress failed to make a connection to the Chrome DevTools Protocol after retrying for 50 seconds.
This usually indicates there was a problem opening the FooBrowser browser.
The CDP port requested was ${chalk.yellow('12345')}.
Error details:
`
it('rejects if CDP connection fails', () => {
const innerErr = new Error('cdp connection failure')
sinon.stub(connect, 'createRetryingSocket').callsArgWith(1, innerErr)
const p = protocol.getWsTargetFor(12345, 'FooBrowser')
return expect(p).to.eventually.be.rejected
.and.property('message').include(expectedCdpFailedError)
.and.include(innerErr.message)
})
it('rejects if CRI.List fails', () => {
const innerErr = new Error('cdp connection failure')
sinon.stub(Bluebird, 'delay').resolves()
sinon.stub(CRI, 'List')
.withArgs({ host, port: 12345, getDelayMsForRetry: sinon.match.func })
.rejects(innerErr)
context('._connectAsync', () => {
it('creates a retrying socket to test the connection', async function () {
const end = sinon.stub()
sinon.stub(connect, 'createRetryingSocket').callsArgWith(1, null, { end })
const p = protocol.getWsTargetFor(12345, 'FooBrowser')
const opts = {
host: '127.0.0.1',
port: 3333,
}
return expect(p).to.eventually.be.rejected
.and.property('message').include(expectedCdpFailedError)
.and.include(innerErr.message)
})
it('returns the debugger URL of the first about:blank tab', async () => {
const targets = [
{
type: 'page',
url: 'chrome://newtab',
webSocketDebuggerUrl: 'foo',
},
{
type: 'page',
url: 'about:blank',
webSocketDebuggerUrl: 'bar',
},
]
const end = sinon.stub()
sinon.stub(CRI, 'List')
.withArgs({ host, port: 12345, getDelayMsForRetry: sinon.match.func })
.resolves(targets)
sinon.stub(connect, 'createRetryingSocket').callsArgWith(1, null, { end })
const p = protocol.getWsTargetFor(12345, 'FooBrowser')
await expect(p).to.eventually.equal('bar')
await protocol._connectAsync(opts)
expect(end).to.be.calledOnce
})
})
context('CRI.List', () => {
const port = 1234
const targets = [
{
type: 'page',
url: 'chrome://newtab',
webSocketDebuggerUrl: 'foo',
},
{
type: 'page',
url: 'about:blank',
webSocketDebuggerUrl: 'ws://debug-url',
},
]
it('retries several times if starting page cannot be found', async () => {
const end = sinon.stub()
sinon.stub(connect, 'createRetryingSocket').callsArgWith(1, null, { end })
const criList = sinon.stub(CRI, 'List')
.withArgs({ host, port, getDelayMsForRetry: sinon.match.func }).resolves(targets)
.onFirstCall().resolves([])
.onSecondCall().resolves([])
.onThirdCall().resolves(targets)
const targetUrl = await protocol.getWsTargetFor(port, 'FooBrowser')
expect(criList).to.have.been.calledThrice
expect(targetUrl).to.equal('ws://debug-url')
})
it('logs correctly if retries occur while connecting to CDP and while listing CRI targets', async () => {
const log = sinon.spy(console, 'log')
const end = sinon.stub()
// fail 20 times to get 2 log lines from connect failures
sinon.stub(connect, 'createRetryingSocket').callsFake((opts, cb) => {
_.times(20, (i) => {
opts.getDelayMsForRetry(i, new Error)
})
// @ts-ignore
return cb(null, { end })
})
sinon.stub(Bluebird, 'delay').resolves()
// fail an additional 2 times on CRI.List
const criList = sinon.stub(CRI, 'List')
.withArgs({ host, port, getDelayMsForRetry: sinon.match.func }).resolves(targets)
.onFirstCall().resolves([])
.onSecondCall().resolves([])
.onThirdCall().resolves(targets)
const targetUrl = await protocol.getWsTargetFor(port, 'FooBrowser')
expect(criList).to.have.been.calledThrice
expect(targetUrl).to.equal('ws://debug-url')
// 2 from connect failing, 2 from CRI.List failing
expect(log).to.have.callCount(4)
log.getCalls().forEach((log, i) => {
const line = stripAnsi(log.args[0])
expect(line).to.include(`Still waiting to connect to FooBrowser, retrying in 1 second (attempt ${i + 18}/62)`)
})
})
})
})
@@ -26,6 +26,7 @@ describe('lib/open_project', () => {
this.onError = sinon.stub()
sinon.stub(browsers, 'get').resolves()
sinon.stub(browsers, 'open')
sinon.stub(browsers, 'connectToNewSpec')
sinon.stub(ProjectBase.prototype, 'initializeConfig').resolves({
e2e: {
specPattern: 'cypress/integration/**/*',
@@ -229,6 +230,11 @@ describe('lib/open_project', () => {
})
})
})
it('calls connectToNewSpec when shouldLaunchNewTab is set', async function () {
await openProject.launch(this.browser, this.spec, { shouldLaunchNewTab: true })
expect(browsers.connectToNewSpec.lastCall.args[0]).to.be.equal(this.browser)
})
})
})
})
+1
View File
@@ -6,6 +6,7 @@ export interface LaunchOpts {
url?: string
automationMiddleware?: AutomationMiddleware
projectRoot?: string
shouldLaunchNewTab?: boolean
onBrowserClose?: (...args: unknown[]) => void
onBrowserOpen?: (...args: unknown[]) => void
onError?: (err: Error) => void
-34
View File
@@ -19,44 +19,10 @@ exports['e2e cdp / handles disconnections as expected'] = `
e2e remote debugging disconnect
reconnects as expected
There was an error reconnecting to the Chrome DevTools protocol. Please restart the browser.
Error: connect ECONNREFUSED 127.0.0.1:7777
[stack trace lines]
(Results)
Tests: 0
Passing: 0
Failing: 1
Pending: 0
Skipped: 0
Screenshots: 0
Video: true
Duration: X seconds
Spec Ran: spec.ts
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/spec.ts.mp4 (X second)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
spec.ts XX:XX - - 1 - -
1 of 1 failed (100%) XX:XX - - 1 - -
`
+165 -78
View File
@@ -164,7 +164,171 @@ exports['deprecated before:browser:launch args / no mutate return'] = `
`
exports['deprecated before:browser:launch args / concat return returns once per spec'] = `
exports['deprecated before:browser:launch args / fails when adding unknown properties to launchOptions'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (app.cy.js)
Searched: cypress/e2e/app.cy.js
Running: app.cy.js (1 of 1)
The \`launchOptions\` object returned by your plugin's \`before:browser:launch\` handler contained unexpected properties:
- foo
- width
- height
\`launchOptions\` may only contain the properties:
- preferences
- extensions
- args
https://on.cypress.io/browser-launch-api
`
exports['deprecated before:browser:launch args / displays errors thrown and aborts the run'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 2 found (app.cy.js, app_spec2.js)
Searched: cypress/e2e/app.cy.js, cypress/e2e/app_spec2.js
Running: app.cy.js (1 of 2)
Error thrown from plugins handler
Error: Error thrown from plugins handler
[stack trace lines]
`
exports['deprecated before:browser:launch args / displays promises rejected and aborts the run'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 2 found (app.cy.js, app_spec2.js)
Searched: cypress/e2e/app.cy.js, cypress/e2e/app_spec2.js
Running: app.cy.js (1 of 2)
Promise rejected from plugins handler
Error: Promise rejected from plugins handler
[stack trace lines]
`
exports['deprecated before:browser:launch args / concat return returns once per test run - [firefox,chromium]'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 2 found (app.cy.js, app_spec2.js)
Searched: cypress/e2e/app.cy.js, cypress/e2e/app_spec2.js
Running: app.cy.js (1 of 2)
Deprecation Warning: The \`before:browser:launch\` plugin event changed its signature in version \`4.0.0\`
The \`before:browser:launch\` plugin event switched from yielding the second argument as an \`array\` of browser arguments to an options \`object\` with an \`args\` property.
We've detected that your code is still using the previous, deprecated interface signature.
This code will not work in a future version of Cypress. Please see the upgrade guide: https://on.cypress.io/deprecated-before-browser-launch-args
asserts on browser args
1 passing
(Results)
Tests: 1
Passing: 1
Failing: 0
Pending: 0
Skipped: 0
Screenshots: 0
Video: false
Duration: X seconds
Spec Ran: app.cy.js
Running: app_spec2.js (2 of 2)
2 - asserts on browser args
1 passing
(Results)
Tests: 1
Passing: 1
Failing: 0
Pending: 0
Skipped: 0
Screenshots: 0
Video: false
Duration: X seconds
Spec Ran: app_spec2.js
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
app.cy.js XX:XX 1 1 - - -
app_spec2.js XX:XX 1 1 - - -
All specs passed! XX:XX 2 2 - - -
`
exports['deprecated before:browser:launch args / concat return returns once per spec - [electron]'] = `
====================================================================================================
@@ -257,80 +421,3 @@ This code will not work in a future version of Cypress. Please see the upgrade g
`
exports['deprecated before:browser:launch args / fails when adding unknown properties to launchOptions'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 1 found (app.cy.js)
Searched: cypress/e2e/app.cy.js
Running: app.cy.js (1 of 1)
The \`launchOptions\` object returned by your plugin's \`before:browser:launch\` handler contained unexpected properties:
- foo
- width
- height
\`launchOptions\` may only contain the properties:
- preferences
- extensions
- args
https://on.cypress.io/browser-launch-api
`
exports['deprecated before:browser:launch args / displays errors thrown and aborts the run'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 2 found (app.cy.js, app_spec2.js)
Searched: cypress/e2e/app.cy.js, cypress/e2e/app_spec2.js
Running: app.cy.js (1 of 2)
Error thrown from plugins handler
Error: Error thrown from plugins handler
[stack trace lines]
`
exports['deprecated before:browser:launch args / displays promises rejected and aborts the run'] = `
====================================================================================================
(Run Starting)
Cypress: 1.2.3
Browser: FooBrowser 88
Specs: 2 found (app.cy.js, app_spec2.js)
Searched: cypress/e2e/app.cy.js, cypress/e2e/app_spec2.js
Running: app.cy.js (1 of 2)
Promise rejected from plugins handler
Error: Promise rejected from plugins handler
[stack trace lines]
`
@@ -0,0 +1,23 @@
/* eslint-disable
mocha/no-global-tests,
no-undef
*/
const req = (win) => {
return new Promise((resolve, reject) => {
const xhr = new win.XMLHttpRequest()
xhr.open('GET', 'http://localhost:1515/cached/')
xhr.onload = () => {
return resolve(win)
}
xhr.onerror = reject
return xhr.send()
})
}
it('makes cached request', () => {
cy.visit('http://localhost:1515')
.then(req) // this creates the disk cache
})
@@ -0,0 +1,23 @@
/* eslint-disable
mocha/no-global-tests,
no-undef
*/
const req = (win) => {
return new Promise((resolve, reject) => {
const xhr = new win.XMLHttpRequest()
xhr.open('GET', 'http://localhost:1515/cached/')
xhr.onload = () => {
return resolve(win)
}
xhr.onerror = reject
return xhr.send()
})
}
it('makes cached request', () => {
cy.visit('http://localhost:1515')
.then(req) // this should hit our server even though cached in the first spec
})
@@ -3,8 +3,8 @@ describe('e2e remote debugging disconnect', () => {
// 1 probing connection and 1 real connection should have been made during startup
cy.task('get:stats')
.should('include', {
totalConnectionCount: 2,
currentConnectionCount: 1,
totalConnectionCount: 4,
currentConnectionCount: 2,
})
// now, kill all CDP sockets
@@ -19,10 +19,11 @@ describe('e2e remote debugging disconnect', () => {
})
.should('have.keys', ['protocolVersion', 'product', 'revision', 'userAgent', 'jsVersion'])
// TODO: We're only reconnecting the page client. See if we can find a way to reconnect the browser client
// evidence of a reconnection:
cy.task('get:stats')
.should('include', {
totalConnectionCount: 3,
totalConnectionCount: 6,
currentConnectionCount: 1,
})
})
+40
View File
@@ -0,0 +1,40 @@
const express = require('express')
const Fixtures = require('../lib/fixtures')
const systemTests = require('../lib/system-tests').default
const e2ePath = Fixtures.projectPath('e2e')
let requestsForCache = 0
const onServer = function (app) {
app.use(express.static(e2ePath, {
// force caching to happen
maxAge: 3600000,
}))
app.get('/cached', (req, res) => {
requestsForCache += 1
return res
.set('cache-control', 'public, max-age=3600')
.send('this response will be disk cached')
})
}
describe('e2e browser reset', () => {
systemTests.setup({
servers: {
port: 1515,
onServer,
},
})
systemTests.it('executes two specs with a cached call', {
project: 'e2e',
spec: 'browser_reset_first_spec.cy.js,browser_reset_second_spec.cy.js',
onRun: async (exec) => {
await exec()
expect(requestsForCache).to.eq(2)
},
})
})
+8 -1
View File
@@ -50,7 +50,7 @@ describe('deprecated before:browser:launch args', () => {
psInclude: ['--foo', '--bar'],
})
systemTests.it('concat return returns once per spec', {
systemTests.it.only('concat return returns once', {
// TODO: implement webPreferences.additionalArgs here
// once we decide if/what we're going to make the implemenation
// SUGGESTION: add this to Cypress.browser.args which will capture
@@ -64,6 +64,13 @@ describe('deprecated before:browser:launch args', () => {
project: beforeBrowserLaunchProject,
spec: 'app.cy.js,app_spec2.js',
snapshot: true,
onRun: (exec, browser) => {
if (browser === 'electron') {
return exec({ originalTitle: `deprecated before:browser:launch args / concat return returns once per spec - [electron]` })
}
return exec({ originalTitle: `deprecated before:browser:launch args / concat return returns once per test run - [firefox,chromium]` })
},
stdoutInclude: 'Deprecation Warning:',
})
+16 -4
View File
@@ -8560,6 +8560,13 @@
dependencies:
"@types/node" "*"
"@types/chrome-remote-interface@0.31.4":
version "0.31.4"
resolved "https://registry.yarnpkg.com/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.4.tgz#d07b34f4af0c7294c5b0164780b3d7bfce078155"
integrity sha512-DJHDwimNqCgAyG5gFmr6Y265pe967u3mnkeMVc0iHuf04PHzTgFypA2AjxSvtkM/pogqWxvfRYXy9Wa5Dj0U1g==
dependencies:
devtools-protocol "0.0.927104"
"@types/chrome@0.0.101":
version "0.0.101"
resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.101.tgz#8b6f7d4f1d4890ba7d950f8492725fa7ba9ab910"
@@ -15063,10 +15070,10 @@ chrome-har-capturer@0.13.4:
chrome-remote-interface "^0.25.7"
commander "2.x.x"
chrome-remote-interface@0.28.2:
version "0.28.2"
resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.28.2.tgz#6be3554d2c227ff07eb74baa7e5d4911da12a5a6"
integrity sha512-F7mjof7rWvRNsJqhVXuiFU/HWySCxTA9tzpLxUJxVfdLkljwFJ1aMp08AnwXRmmP7r12/doTDOMwaNhFCJsacw==
chrome-remote-interface@0.31.1:
version "0.31.1"
resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.31.1.tgz#87c37c81f10d9f0832b6d6a42e52ac2c7ebb4008"
integrity sha512-cvNTnXfx4kYCaeh2sEKrdlqZsYRleACPL47O8LrrjihVfBQbfPmf03vVqSSm7SIeqyo2P77ZXovrBAs4D/nopQ==
dependencies:
commander "2.11.x"
ws "^7.2.0"
@@ -18073,6 +18080,11 @@ devtools-protocol@0.0.839267:
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.839267.tgz#6f437369e4664dbd9849723b886f8cedf28607ac"
integrity sha512-1DcPo/3PGJVw3INnq2NXO4onJiNFhWJW4YmSRQIiuDK/sW5OD1/LfXCcetrPvMvOBnz5c22DlpnpbYkJRE8MZw==
devtools-protocol@0.0.927104:
version "0.0.927104"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.927104.tgz#3bba0fca644bcdce1bcebb10ae392ab13428a7a0"
integrity sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA==
dezalgo@^1.0.0, dezalgo@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"