mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-23 23:49:43 -05:00
feat: Close and reopen a new tab in between tests to get around a memory leak (#19915)
This commit is contained in:
+5
-1
@@ -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:
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
},
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -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:',
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user