UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

595 lines (473 loc) 18.7 kB
require('../../spec_helper') const _ = require('lodash') const EE = require('events') const la = require('lazy-ass') const check = require('check-more-types') const menu = require(`${root}../lib/gui/menu`) const plugins = require(`${root}../lib/plugins`) const Windows = require(`${root}../lib/gui/windows`) const electron = require(`${root}../lib/browsers/electron`) const savedState = require(`${root}../lib/saved_state`) const { Automation } = require(`${root}../lib/automation`) const ELECTRON_PID = 10001 describe('lib/browsers/electron', () => { beforeEach(function () { this.url = 'https://foo.com' this.state = {} this.options = { some: 'var', projectRoot: '/foo/', onWarning: sinon.stub().returns(), } this.automation = new Automation('foo', 'bar', 'baz') this.win = _.extend(new EE(), { isDestroyed () { return false }, close: sinon.stub(), loadURL: sinon.stub(), focusOnWebView: sinon.stub(), webContents: { session: { cookies: { get: sinon.stub(), set: sinon.stub(), remove: sinon.stub(), }, on: sinon.stub(), }, getOSProcessId: sinon.stub().returns(ELECTRON_PID), 'debugger': { attach: sinon.stub().returns(), sendCommand: sinon.stub().resolves(), on: sinon.stub().returns(), }, }, }) sinon.stub(Windows, 'installExtension').returns() sinon.stub(Windows, 'removeAllExtensions').returns() this.stubForOpen = function () { sinon.stub(electron, '_render').resolves(this.win) sinon.stub(plugins, 'has') sinon.stub(plugins, 'execute') return savedState.create() .then((state) => { la(check.fn(state.get), 'state is missing .get to stub', state) return sinon.stub(state, 'get').resolves(this.state) }) } }) context('.open', () => { beforeEach(function () { return this.stubForOpen() }) it('calls render with url, state, and options', function () { return electron.open('electron', this.url, this.options, this.automation) .then(() => { let options = electron._defaultOptions(this.options.projectRoot, this.state, this.options) options = Windows.defaults(options) const keys = _.keys(electron._render.firstCall.args[3]) expect(_.keys(options)).to.deep.eq(keys) expect(electron._render).to.be.calledWith( this.url, this.options.projectRoot, ) }) }) it('returns custom object emitter interface', function () { return electron.open('electron', this.url, this.options, this.automation) .then((obj) => { expect(obj.browserWindow).to.eq(this.win) expect(obj.kill).to.be.a('function') expect(obj.removeAllListeners).to.be.a('function') expect(this.win.webContents.getOSProcessId).to.be.calledOnce expect(obj.pid).to.deep.eq([ELECTRON_PID]) }) }) it('is noop when before:browser:launch yields null', function () { plugins.has.returns(true) plugins.execute.resolves(null) return electron.open('electron', this.url, this.options, this.automation) .then(() => { const options = electron._render.firstCall.args[3] expect(options).to.include.keys('onFocus', 'onNewWindow', 'onCrashed') }) }) // https://github.com/cypress-io/cypress/issues/1992 it('it merges in user preferences without removing essential options', function () { plugins.has.returns(true) plugins.execute.withArgs('before:browser:launch').resolves({ preferences: { foo: 'bar', }, }) return electron.open('electron', this.url, this.options, this.automation) .then(() => { const options = electron._render.firstCall.args[3] expect(options).to.include.keys('foo', 'onFocus', 'onNewWindow', 'onCrashed') }) }) it('installs supplied extensions from before:browser:launch and warns on failure', function () { plugins.has.returns(true) plugins.execute.resolves({ extensions: ['foo', 'bar'] }) Windows.installExtension.withArgs(sinon.match.any, 'bar').throws() return electron.open('electron', this.url, this.options, this.automation) .then(() => { expect(Windows.removeAllExtensions).to.be.calledOnce expect(Windows.installExtension).to.be.calledTwice expect(Windows.installExtension).to.be.calledWith(sinon.match.any, 'foo') expect(Windows.installExtension).to.be.calledWith(sinon.match.any, 'bar') expect(this.options.onWarning).to.be.calledOnce const warning = this.options.onWarning.firstCall.args[0].message expect(warning).to.contain('Electron').and.contain('bar') this.win.emit('closed') // called once before installing extensions, once on exit expect(Windows.removeAllExtensions).to.be.calledTwice }) }) }) context('._launch', () => { beforeEach(() => { sinon.stub(menu, 'set') sinon.stub(electron, '_attachDebugger').resolves() sinon.stub(electron, '_clearCache').resolves() sinon.stub(electron, '_setProxy').resolves() sinon.stub(electron, '_setUserAgent') }) it('sets menu.set whether or not its in headless mode', function () { return electron._launch(this.win, this.url, this.automation, { show: true }) .then(() => { expect(menu.set).to.be.calledWith({ withDevTools: true }) }).then(() => { menu.set.reset() return electron._launch(this.win, this.url, this.automation, { show: false }) }).then(() => { expect(menu.set).not.to.be.called }) }) it('sets user agent if options.userAgent', function () { return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(electron._setUserAgent).not.to.be.called }).then(() => { return electron._launch(this.win, this.url, this.automation, { userAgent: 'foo' }) }).then(() => { expect(electron._setUserAgent).to.be.calledWith(this.win.webContents, 'foo') }) }) it('sets proxy if options.proxyServer', function () { return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(electron._setProxy).not.to.be.called }).then(() => { return electron._launch(this.win, this.url, this.automation, { proxyServer: 'foo' }) }).then(() => { expect(electron._setProxy).to.be.calledWith(this.win.webContents, 'foo') }) }) it('calls win.loadURL with url', function () { return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(this.win.loadURL).to.be.calledWith(this.url) }) }) it('resolves with win', function () { return electron._launch(this.win, this.url, this.automation, this.options) .then((win) => { expect(win).to.eq(this.win) }) }) it('pushes create:download when download begins', function () { const downloadItem = { getETag: () => '1', getFilename: () => 'file.csv', getMimeType: () => 'text/csv', getURL: () => 'http://localhost:1234/file.csv', once: sinon.stub(), } this.win.webContents.session.on.withArgs('will-download').yields({}, downloadItem) this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(this.automation.push).to.be.calledWith('create:download', { id: '1', filePath: 'downloads/file.csv', mime: 'text/csv', url: 'http://localhost:1234/file.csv', }) }) }) it('pushes complete:download when download is done', function () { const downloadItem = { getETag: () => '1', getFilename: () => 'file.csv', getMimeType: () => 'text/csv', getURL: () => 'http://localhost:1234/file.csv', once: sinon.stub().yields(), } this.win.webContents.session.on.withArgs('will-download').yields({}, downloadItem) this.options.downloadsFolder = 'downloads' sinon.stub(this.automation, 'push') return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(this.automation.push).to.be.calledWith('complete:download', { id: '1', }) }) }) it('sets download behavior', function () { this.options.downloadsFolder = 'downloads' return electron._launch(this.win, this.url, this.automation, this.options) .then(() => { expect(this.win.webContents.debugger.sendCommand).to.be.calledWith('Page.setDownloadBehavior', { behavior: 'allow', downloadPath: 'downloads', }) }) }) }) context('._render', () => { beforeEach(function () { this.newWin = {} sinon.stub(menu, 'set') sinon.stub(electron, '_setProxy').resolves() sinon.stub(electron, '_launch').resolves() return sinon.stub(Windows, 'create') .withArgs(this.options.projectRoot, this.options) .returns(this.newWin) }) it('creates window instance and calls launch with window', function () { return electron._render(this.url, this.options.projectRoot, this.automation, this.options) .then(() => { expect(Windows.create).to.be.calledWith(this.options.projectRoot, this.options) expect(electron._launch).to.be.calledWith(this.newWin, this.url, this.automation, this.options) }) }) it('registers onRequest automation middleware', function () { sinon.spy(this.automation, 'use') return electron._render(this.url, this.options.projectRoot, this.automation, this.options) .then(() => { expect(this.automation.use).to.be.called expect(this.automation.use.lastCall.args[0].onRequest).to.be.a('function') }) }) }) context('._defaultOptions', () => { beforeEach(() => { return sinon.stub(menu, 'set') }) it('uses default width if there isn\'t one saved', function () { const opts = electron._defaultOptions('/foo', this.state, this.options) expect(opts.width).to.eq(1280) }) it('uses saved width if there is one', function () { const opts = electron._defaultOptions('/foo', { browserWidth: 1024 }, this.options) expect(opts.width).to.eq(1024) }) it('uses default height if there isn\'t one saved', function () { const opts = electron._defaultOptions('/foo', this.state, this.options) expect(opts.height).to.eq(720) }) it('uses saved height if there is one', function () { const opts = electron._defaultOptions('/foo', { browserHeight: 768 }, this.options) expect(opts.height).to.eq(768) }) it('uses saved x if there is one', function () { const opts = electron._defaultOptions('/foo', { browserX: 200 }, this.options) expect(opts.x).to.eq(200) }) it('uses saved y if there is one', function () { const opts = electron._defaultOptions('/foo', { browserY: 300 }, this.options) expect(opts.y).to.eq(300) }) it('tracks browser state', function () { const opts = electron._defaultOptions('/foo', { browserY: 300 }, this.options) const args = _.pick(opts.trackState, 'width', 'height', 'x', 'y', 'devTools') expect(args).to.deep.eq({ width: 'browserWidth', height: 'browserHeight', x: 'browserX', y: 'browserY', devTools: 'isBrowserDevToolsOpen', }) }) it('.onFocus', function () { let opts = electron._defaultOptions('/foo', this.state, { show: true }) opts.onFocus() expect(menu.set).to.be.calledWith({ withDevTools: true }) menu.set.reset() opts = electron._defaultOptions('/foo', this.state, { show: false }) opts.onFocus() expect(menu.set).not.to.be.called }) describe('.onNewWindow', () => { beforeEach(function () { return sinon.stub(electron, '_launchChild').resolves(this.win) }) it('passes along event, url, parent window and options', function () { const opts = electron._defaultOptions(this.options.projectRoot, this.state, this.options, this.automation) const event = {} const parentWindow = { on: sinon.stub(), } opts.onNewWindow.call(parentWindow, event, this.url) expect(electron._launchChild).to.be.calledWith( event, this.url, parentWindow, this.options.projectRoot, this.state, this.options, this.automation, ) }) it('adds pid of new BrowserWindow to pid list', function () { const opts = electron._defaultOptions(this.options.projectRoot, this.state, this.options) const NEW_WINDOW_PID = ELECTRON_PID * 2 const child = _.cloneDeep(this.win) child.webContents.getOSProcessId = sinon.stub().returns(NEW_WINDOW_PID) electron._launchChild.resolves(child) return this.stubForOpen() .then(() => { return electron.open('electron', this.url, opts, this.automation) }).then((instance) => { return opts.onNewWindow.call(this.win, {}, this.url) .then(() => { expect(instance.pid).to.deep.eq([ELECTRON_PID, NEW_WINDOW_PID]) }) }) }) }) }) // TODO: these all need to be updated context.skip('._launchChild', () => { beforeEach(function () { this.childWin = _.extend(new EE(), { close: sinon.stub(), isDestroyed: sinon.stub().returns(false), webContents: new EE(), }) Windows.create.onCall(1).resolves(this.childWin) this.event = { preventDefault: sinon.stub() } this.win.getPosition = () => { return [4, 2] } this.openNewWindow = (options) => { // eslint-disable-next-line no-undef return launcher.launch('electron', this.url, options).then(() => { return this.win.webContents.emit('new-window', this.event, 'some://other.url') }) } }) it('prevents default', function () { return this.openNewWindow().then(() => { expect(this.event.preventDefault).to.be.called }) }) it('creates child window', function () { return this.openNewWindow().then(() => { const args = Windows.create.lastCall.args[0] expect(Windows.create).to.be.calledTwice expect(args.url).to.equal('some://other.url') expect(args.minWidth).to.equal(100) expect(args.minHeight).to.equal(100) }) }) it('offsets it from parent by 100px', function () { return this.openNewWindow().then(() => { const args = Windows.create.lastCall.args[0] expect(args.x).to.equal(104) expect(args.y).to.equal(102) }) }) it('passes along web security', function () { return this.openNewWindow({ chromeWebSecurity: false }).then(() => { const args = Windows.create.lastCall.args[0] expect(args.chromeWebSecurity).to.be.false }) }) it('sets unique PROJECT type on each new window', function () { return this.openNewWindow().then(() => { const firstArgs = Windows.create.lastCall.args[0] expect(firstArgs.type).to.match(/^PROJECT-CHILD-\d/) this.win.webContents.emit('new-window', this.event, 'yet://another.url') const secondArgs = Windows.create.lastCall.args[0] expect(secondArgs.type).to.match(/^PROJECT-CHILD-\d/) expect(firstArgs.type).not.to.equal(secondArgs.type) }) }) it('set newGuest on child window', function () { return this.openNewWindow() .then(() => { return Promise.delay(1) }).then(() => { expect(this.event.newGuest).to.equal(this.childWin) }) }) it('sets menu with dev tools on creation', function () { return this.openNewWindow().then(() => { // once for main window, once for child expect(menu.set).to.be.calledTwice expect(menu.set).to.be.calledWith({ withDevTools: true }) }) }) it('sets menu with dev tools on focus', function () { return this.openNewWindow().then(() => { Windows.create.lastCall.args[0].onFocus() // once for main window, once for child, once for focus expect(menu.set).to.be.calledThrice expect(menu.set).to.be.calledWith({ withDevTools: true }) }) }) it('it closes the child window when the parent window is closed', function () { return this.openNewWindow() .then(() => { return Promise.delay(1) }).then(() => { this.win.emit('close') expect(this.childWin.close).to.be.called }) }) it('does not close the child window when it is already destroyed', function () { return this.openNewWindow() .then(() => { return Promise.delay(1) }).then(() => { this.childWin.isDestroyed.returns(true) this.win.emit('close') expect(this.childWin.close).not.to.be.called }) }) it('does the same things for children of the child window', function () { this.grandchildWin = _.extend(new EE(), { close: sinon.stub(), isDestroyed: sinon.stub().returns(false), webContents: new EE(), }) Windows.create.onCall(2).resolves(this.grandchildWin) this.childWin.getPosition = () => { return [104, 102] } return this.openNewWindow().then(() => { this.childWin.webContents.emit('new-window', this.event, 'yet://another.url') const args = Windows.create.lastCall.args[0] expect(Windows.create).to.be.calledThrice expect(args.url).to.equal('yet://another.url') expect(args.type).to.match(/^PROJECT-CHILD-\d/) expect(args.x).to.equal(204) expect(args.y).to.equal(202) }) }) }) context('._setProxy', () => { it('sets proxy rules for webContents', () => { const webContents = { session: { setProxy: sinon.stub().resolves(), }, } return electron._setProxy(webContents, 'proxy rules') .then(() => { expect(webContents.session.setProxy).to.be.calledWith({ proxyRules: 'proxy rules', proxyBypassRules: '<-loopback>', }) }) }) }) })