diff --git a/cli/package.json b/cli/package.json index ad9d1cbb0b9b..012ee33afe36 100644 --- a/cli/package.json +++ b/cli/package.json @@ -86,7 +86,7 @@ "mocked-env": "1.2.4", "nock": "12.0.2", "postinstall-postinstall": "2.0.0", - "proxyquire": "2.1.0", + "proxyquire": "2.1.3", "resolve-pkg": "2.0.0", "shelljs": "0.8.3", "sinon": "7.2.2", diff --git a/package.json b/package.json index 6e5e31f4e819..47758b8d7adb 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "prefixed-list": "1.0.1", "pretty-ms": "6.0.1", "print-arch": "1.0.0", - "proxyquire": "2.1.0", + "proxyquire": "2.1.3", "ramda": "0.24.1", "shelljs": "0.8.3", "shx": "0.3.2", diff --git a/packages/reporter/src/test/test.tsx b/packages/reporter/src/test/test.tsx index a6555269f660..f3cf57690123 100644 --- a/packages/reporter/src/test/test.tsx +++ b/packages/reporter/src/test/test.tsx @@ -70,7 +70,10 @@ class Test extends Component { if (appState.autoScrollingEnabled && appState.isRunning && shouldRender && isActive != null) { window.requestAnimationFrame(() => { - scroller.scrollIntoView(this.containerRef.current as HTMLElement) + // since this executes async in a RAF the ref might be null + if (this.containerRef.current) { + scroller.scrollIntoView(this.containerRef.current as HTMLElement) + } }) } } diff --git a/packages/server/lib/browsers/firefox-util.ts b/packages/server/lib/browsers/firefox-util.ts index 3c4078d264ac..41b329a90c69 100644 --- a/packages/server/lib/browsers/firefox-util.ts +++ b/packages/server/lib/browsers/firefox-util.ts @@ -177,6 +177,10 @@ export default { const { browser } = foxdriver + browser.on('error', (err) => { + debug('received error from foxdriver connection, ignoring %o', err) + }) + forceGcCc = () => { let gcDuration; let ccDuration diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index ff71136ba9de..97a3da67d053 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -9,7 +9,10 @@ import FirefoxProfile from 'firefox-profile' import firefoxUtil from './firefox-util' import utils from './utils' import * as launcherDebug from '@packages/launcher/lib/log' -import { Browser } from './types' +import { Browser, BrowserInstance } from './types' +import { EventEmitter } from 'events' +import os from 'os' +import treeKill from 'tree-kill' const errors = require('../errors') const debug = Debug('cypress:server:browsers:firefox') @@ -287,7 +290,23 @@ const defaultPreferences = { 'marionette.log.level': launcherDebug.log.enabled ? 'Debug' : undefined, } -export async function open (browser: Browser, url, options: any = {}) { +export function _createDetachedInstance (browserInstance: BrowserInstance): 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 => { + treeKill(browserInstance.pid, (err?, result?) => { + debug('force-exit of process tree complete %o', { err, result }) + detachedInstance.emit('exit') + }) + } + + return detachedInstance +} + +export async function open (browser: Browser, url, options: any = {}): Bluebird { const defaultLaunchOptions = utils.getDefaultLaunchOptions({ extensions: [] as string[], preferences: _.extend({}, defaultPreferences), @@ -347,7 +366,7 @@ export async function open (browser: Browser, url, options: any = {}) { launchOptions, ] = await Bluebird.all([ utils.ensureCleanCache(browser, options.isTextTerminal), - utils.writeExtension(browser, options.isTextTerminal, options.proxyUrl, options.socketIoRoute, options.onScreencastFrame), + utils.writeExtension(browser, options.isTextTerminal, options.proxyUrl, options.socketIoRoute), utils.executeBeforeBrowserLaunch(browser, defaultLaunchOptions, options), ]) @@ -419,5 +438,11 @@ export async function open (browser: Browser, url, options: any = {}) { 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 } diff --git a/packages/server/lib/browsers/types.ts b/packages/server/lib/browsers/types.ts index 7b11010af87e..fdfdd6498cbd 100644 --- a/packages/server/lib/browsers/types.ts +++ b/packages/server/lib/browsers/types.ts @@ -1,7 +1,13 @@ import { FoundBrowser } from '@packages/launcher' +import { EventEmitter } from 'events' export type Browser = FoundBrowser & { majorVersion: number isHeadless: boolean isHeaded: boolean } + +export type BrowserInstance = EventEmitter & { + kill: () => void + pid: number +} diff --git a/packages/server/lib/browsers/utils.ts b/packages/server/lib/browsers/utils.ts index 1870beeca509..09d0eedfd1df 100644 --- a/packages/server/lib/browsers/utils.ts +++ b/packages/server/lib/browsers/utils.ts @@ -205,12 +205,12 @@ export = { launch: launcher.launch, - writeExtension (browser, isTextTerminal, proxyUrl, socketIoRoute, onScreencastFrame) { + writeExtension (browser, isTextTerminal, proxyUrl, socketIoRoute) { debug('writing extension') // debug('writing extension to chrome browser') // get the string bytes for the final extension file - return extension.setHostAndPath(proxyUrl, socketIoRoute, onScreencastFrame) + return extension.setHostAndPath(proxyUrl, socketIoRoute) .then((str) => { const extensionDest = getExtensionDir(browser, isTextTerminal) const extensionBg = path.join(extensionDest, 'background.js') @@ -231,7 +231,7 @@ export = { debug('getBrowsers') return launcher.detect() - .then((browsers = []) => { + .then((browsers: FoundBrowser[] = []) => { let majorVersion debug('found browsers %o', { browsers }) diff --git a/packages/server/package.json b/packages/server/package.json index c5973f1a15b7..9365a2fb01e7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -96,7 +96,7 @@ "pluralize": "8.0.0", "ramda": "0.24.1", "randomstring": "1.1.5", - "resolve": "1.13.1", + "resolve": "1.17.0", "return-deep-diff": "0.4.0", "sanitize-filename": "1.6.3", "semver": "6.3.0", @@ -110,11 +110,12 @@ "term-size": "2.1.0", "tough-cookie": "4.0.0", "trash": "5.2.0", + "tree-kill": "1.2.2", "ts-node": "8.5.4", "underscore": "1.9.1", "underscore.string": "3.3.5", "url-parse": "1.4.7", - "uuid": "3.3.2", + "uuid": "3.4.0", "which": "2.0.2", "widest-line": "3.1.0", "winston": "2.4.4" @@ -174,7 +175,7 @@ "mockery": "2.1.0", "multiparty": "4.2.1", "nock": "12.0.2", - "proxyquire": "2.1.0", + "proxyquire": "2.1.3", "react": "16.8.6", "repl.history": "0.1.4", "sinon": "5.1.1", diff --git a/packages/server/test/e2e/1_firefox_spec.ts b/packages/server/test/e2e/1_firefox_spec.ts index 74d8a8f41227..9f42624ce604 100644 --- a/packages/server/test/e2e/1_firefox_spec.ts +++ b/packages/server/test/e2e/1_firefox_spec.ts @@ -68,4 +68,12 @@ describe('e2e firefox', function () { expect(stderr).not.to.include('foxdriver') }, }) + + // NOTE: only an issue on windows + // https://github.com/cypress-io/cypress/issues/6392 + e2e.it.skip('can run multiple specs', { + browser: 'firefox', + project: Fixtures.projectPath('e2e'), + spec: 'simple_spec.coffee,simple_passing_spec.coffee', + }) }) diff --git a/packages/server/test/unit/browsers/firefox_spec.ts b/packages/server/test/unit/browsers/firefox_spec.ts index 6ee69a500218..f2d737cbf479 100644 --- a/packages/server/test/unit/browsers/firefox_spec.ts +++ b/packages/server/test/unit/browsers/firefox_spec.ts @@ -8,6 +8,7 @@ import * as firefox from '../../../lib/browsers/firefox' import { EventEmitter } from 'events' import Marionette from 'marionette-client' import Foxdriver from '@benmalka/foxdriver' +import os from 'os' const mockfs = require('mock-fs') const FirefoxProfile = require('firefox-profile') const utils = require('../../../lib/browsers/utils') @@ -67,6 +68,7 @@ describe('lib/browsers/firefox', () => { const browser = { listTabs: sinon.stub().resolves([foxdriverTab]), request: sinon.stub().withArgs('listTabs').resolves({ tabs: [foxdriverTab] }), + on: sinon.stub(), } foxdriver = { @@ -102,6 +104,11 @@ describe('lib/browsers/firefox', () => { browser: this.browser, } + this.browserInstance = { + // should be high enough to not kill any real PIDs + pid: Number.MAX_SAFE_INTEGER, + } + sinon.stub(process, 'pid').value(1111) protocol.foo = 'bar' @@ -109,7 +116,6 @@ describe('lib/browsers/firefox', () => { sinon.stub(plugins, 'has') sinon.stub(plugins, 'execute') sinon.stub(utils, 'writeExtension').resolves('/path/to/ext') - this.browserInstance = {} sinon.stub(utils, 'launch').resolves(this.browserInstance) sinon.spy(FirefoxProfile.prototype, 'setPreference') sinon.spy(FirefoxProfile.prototype, 'updatePreferences') @@ -201,7 +207,7 @@ describe('lib/browsers/firefox', () => { it('writes extension', function () { return firefox.open(this.browser, 'http://', this.options).then(() => { - expect(utils.writeExtension).to.be.calledWith(this.options.browser, this.options.isTextTerminal, this.options.proxyUrl, this.options.socketIoRoute, this.options.onScreencastFrame) + expect(utils.writeExtension).to.be.calledWith(this.options.browser, this.options.isTextTerminal, this.options.proxyUrl, this.options.socketIoRoute) }) }) @@ -322,6 +328,29 @@ describe('lib/browsers/firefox', () => { expect(wrapperErr.message).to.include(err.message) }) }) + + context('returns BrowserInstance', function () { + it('from browsers.launch', async function () { + const instance = await firefox.open(this.browser, 'http://', this.options) + + expect(instance).to.eq(this.browserInstance) + }) + + // @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) + + expect(instance).to.not.eq(this.browserInstance) + expect(instance.pid).to.eq(this.browserInstance.pid) + + await new Promise((resolve) => { + // ensure events are wired as expected + instance.on('exit', resolve) + instance.kill() + }) + }) + }) }) context('firefox-util', () => { diff --git a/yarn.lock b/yarn.lock index 8b6d105e67da..182b39619bcc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13931,16 +13931,7 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy@^1.17.0: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-proxy@cypress-io/node-http-proxy#9322b4b69b34f13a6f3874e660a35df3305179c6: +http-proxy@^1.17.0, http-proxy@cypress-io/node-http-proxy#9322b4b69b34f13a6f3874e660a35df3305179c6: version "1.18.0" resolved "https://codeload.github.com/cypress-io/node-http-proxy/tar.gz/9322b4b69b34f13a6f3874e660a35df3305179c6" dependencies: @@ -18078,7 +18069,7 @@ module-lookup-amd@^6.1.0: requirejs "^2.3.5" requirejs-config-file "^3.1.1" -module-not-found-error@^1.0.0: +module-not-found-error@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/module-not-found-error/-/module-not-found-error-1.0.1.tgz#cf8b4ff4f29640674d6cdd02b0e3bc523c2bbdc0" integrity sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA= @@ -19728,7 +19719,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -20429,14 +20420,14 @@ proxy-from-env@1.1.0, proxy-from-env@^1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -proxyquire@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-2.1.0.tgz#c2263a38bf0725f2ae950facc130e27510edce8d" - integrity sha512-kptdFArCfGRtQFv3Qwjr10lwbEV0TBJYvfqzhwucyfEXqVgmnAkyEw/S3FYzR5HI9i5QOq4rcqQjZ6AlknlCDQ== +proxyquire@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-2.1.3.tgz#2049a7eefa10a9a953346a18e54aab2b4268df39" + integrity sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg== dependencies: fill-keys "^1.0.2" - module-not-found-error "^1.0.0" - resolve "~1.8.1" + module-not-found-error "^1.0.1" + resolve "^1.11.1" prr@~1.0.1: version "1.0.1" @@ -21890,10 +21881,10 @@ resolve@1.1.7, resolve@1.1.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16" - integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w== +resolve@1.17.0, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" @@ -21902,20 +21893,6 @@ resolve@^0.6.3: resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.6.3.tgz#dd957982e7e736debdf53b58a4dd91754575dd46" integrity sha1-3ZV5gufnNt699TtYpN2RdUV13UY= -resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== - dependencies: - path-parse "^1.0.6" - -resolve@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== - dependencies: - path-parse "^1.0.5" - responselike@1.0.2, responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -24447,7 +24424,7 @@ traverse-chain@~0.1.0: resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= -tree-kill@^1.1.0: +tree-kill@1.2.2, tree-kill@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -25269,7 +25246,7 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: +uuid@3.4.0, uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==