UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

306 lines (250 loc) 7.51 kB
import Bluebird from 'bluebird' import Debug from 'debug' import _ from 'lodash' import Marionette from 'marionette-client' import { Command } from 'marionette-client/lib/marionette/message.js' import util from 'util' import Foxdriver from '@benmalka/foxdriver' import * as protocol from './protocol' const errors = require('../errors') const debug = Debug('cypress:server:browsers:firefox-util') let forceGcCc: () => Promise<void> let timings = { gc: [] as any[], cc: [] as any[], collections: [] as any[], } const getTabId = (tab) => { return _.get(tab, 'browsingContextID') } const getDelayMsForRetry = (i) => { if (i < 10) { return 100 } if (i < 18) { return 500 } if (i < 63) { return 1000 } return } const getPrimaryTab = Bluebird.method((browser) => { const setPrimaryTab = () => { return browser.listTabs() .then((tabs) => { browser.tabs = tabs return browser.primaryTab = _.first(tabs) }) } // on first connection if (!browser.primaryTab) { return setPrimaryTab() } // `listTabs` will set some internal state, including marking attached tabs // as detached. so use the raw `request` here: return browser.request('listTabs') .then(({ tabs }) => { const firstTab = _.first(tabs) // primaryTab has changed, get all tabs and rediscover first tab if (getTabId(browser.primaryTab.data) !== getTabId(firstTab)) { return setPrimaryTab() } return browser.primaryTab }) }) const attachToTabMemory = Bluebird.method((tab) => { // TODO: figure out why tab.memory is sometimes undefined if (!tab.memory) return if (tab.memory.isAttached) { return } return tab.memory.getState() .then((state) => { if (state === 'attached') { return } tab.memory.on('garbage-collection', ({ data }) => { data.num = timings.collections.length + 1 timings.collections.push(data) debug('received garbage-collection event %o', data) }) return tab.memory.attach() }) }) const logGcDetails = () => { const reducedTimings = { ...timings, collections: _.map(timings.collections, (event) => { return _ .chain(event) .extend({ duration: _.sumBy(event.collections, (collection: any) => { return collection.endTimestamp - collection.startTimestamp }), spread: _.chain(event.collections).thru((collection) => { const first = _.first(collection) const last = _.last(collection) return last.endTimestamp - first.startTimestamp }).value(), }) .pick('num', 'nonincrementalReason', 'reason', 'gcCycleNumber', 'duration', 'spread') .value() }), } debug('forced GC timings %o', util.inspect(reducedTimings, { breakLength: Infinity, maxArrayLength: Infinity, })) debug('forced GC times %o', { gc: reducedTimings.gc.length, cc: reducedTimings.cc.length, collections: reducedTimings.collections.length, }) debug('forced GC averages %o', { gc: _.chain(reducedTimings.gc).sum().divide(reducedTimings.gc.length).value(), cc: _.chain(reducedTimings.cc).sum().divide(reducedTimings.cc.length).value(), collections: _.chain(reducedTimings.collections).sumBy('duration').divide(reducedTimings.collections.length).value(), spread: _.chain(reducedTimings.collections).sumBy('spread').divide(reducedTimings.collections.length).value(), }) debug('forced GC totals %o', { gc: _.sum(reducedTimings.gc), cc: _.sum(reducedTimings.cc), collections: _.sumBy(reducedTimings.collections, 'duration'), spread: _.sumBy(reducedTimings.collections, 'spread'), }) // reset all the timings timings = { gc: [], cc: [], collections: [], } } export default { log () { logGcDetails() }, collectGarbage () { return forceGcCc() }, setup ({ extensions, url, marionettePort, foxdriverPort, }) { return Bluebird.all([ this.setupFoxdriver(foxdriverPort), this.setupMarionette(extensions, url, marionettePort), ]) }, async setupFoxdriver (port) { await protocol._connectAsync({ host: '127.0.0.1', port, getDelayMsForRetry, }) const foxdriver = await Foxdriver.attach('127.0.0.1', port) const { browser } = foxdriver browser.on('error', (err) => { debug('received error from foxdriver connection, ignoring %o', err) }) forceGcCc = () => { let gcDuration; let ccDuration const gc = (tab) => { return () => { // TODO: figure out why tab.memory is sometimes undefined if (!tab.memory) return const start = Date.now() return tab.memory.forceGarbageCollection() .then(() => { gcDuration = Date.now() - start timings.gc.push(gcDuration) }) } } const cc = (tab) => { return () => { // TODO: figure out why tab.memory is sometimes undefined if (!tab.memory) return const start = Date.now() return tab.memory.forceCycleCollection() .then(() => { ccDuration = Date.now() - start timings.cc.push(ccDuration) }) } } debug('forcing GC and CC...') return getPrimaryTab(browser) .then((tab) => { return attachToTabMemory(tab) .then(gc(tab)) .then(cc(tab)) }) .then(() => { debug('forced GC and CC completed %o', { ccDuration, gcDuration }) }) .tapCatch((err) => { debug('firefox RDP error while forcing GC and CC %o', err) }) } }, async setupMarionette (extensions, url, port) { await protocol._connectAsync({ host: '127.0.0.1', port, getDelayMsForRetry, }) const driver = new Marionette.Drivers.Promises({ port, tries: 1, // marionette-client has its own retry logic which we want to avoid }) const sendMarionette = (data) => { return driver.send(new Command(data)) } debug('firefox: navigating page with webdriver') const onError = (from, reject?) => { if (!reject) { reject = (err) => { throw err } } return (err) => { debug('error in marionette %o', { from, err }) reject(errors.get('FIREFOX_MARIONETTE_FAILURE', from, err)) } } await driver.connect() .catch(onError('connection')) await new Bluebird((resolve, reject) => { const _onError = (from) => { return onError(from, reject) } const { tcp } = driver tcp.socket.on('error', _onError('Socket')) tcp.client.on('error', _onError('CommandStream')) sendMarionette({ name: 'WebDriver:NewSession', parameters: { acceptInsecureCerts: true }, }).then(() => { return Bluebird.all(_.map(extensions, (path) => { return sendMarionette({ name: 'Addon:Install', parameters: { path, temporary: true }, }) })) }) .then(() => { return sendMarionette({ name: 'WebDriver:Navigate', parameters: { url }, }) }) .then(resolve) .catch(_onError('commands')) }) // even though Marionette is not used past this point, we have to keep the session open // or else `acceptInsecureCerts` will cease to apply and SSL validation prompts will appear. }, }