UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

810 lines (673 loc) 20.8 kB
require('../../spec_helper') const path = require('path') const _ = require('lodash') const os = require('os') const cp = require('child_process') const Promise = require('bluebird') const { stripIndent } = require('common-tags') const { mockSpawn } = require('spawn-mock') const mockfs = require('mock-fs') const mockedEnv = require('mocked-env') const fs = require(`${lib}/fs`) const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) const xvfb = require(`${lib}/exec/xvfb`) const verify = require(`${lib}/tasks/verify`) const Stdout = require('../../support/stdout') const normalize = require('../../support/normalize') const snapshot = require('../../support/snapshot') const packageVersion = '1.2.3' const cacheDir = '/cache/Cypress' const executablePath = '/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress' const binaryStatePath = '/cache/Cypress/1.2.3/binary_state.json' let stdout let spawnedProcess /* eslint-disable no-octal */ context('lib/tasks/verify', () => { require('mocha-banner').register() beforeEach(() => { stdout = Stdout.capture() spawnedProcess = { code: 0, stderr: sinon.stub(), stdout: '222', } os.platform.returns('darwin') os.release.returns('0.0.0') sinon.stub(util, 'getCacheDir').returns(cacheDir) sinon.stub(util, 'isCi').returns(false) sinon.stub(util, 'pkgVersion').returns(packageVersion) sinon.stub(util, 'exec') sinon.stub(xvfb, 'start').resolves() sinon.stub(xvfb, 'stop').resolves() sinon.stub(xvfb, 'isNeeded').returns(false) sinon.stub(Promise.prototype, 'delay').resolves() sinon.stub(process, 'geteuid').returns(1000) sinon.stub(_, 'random').returns('222') util.exec .withArgs(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) .resolves(spawnedProcess) }) afterEach(() => { Stdout.restore() }) it('has verify task timeout', () => { expect(verify.VERIFY_TEST_RUNNER_TIMEOUT_MS).to.be.gt(10000) }) it('logs error and exits when no version of Cypress is installed', () => { return verify .start() .then(() => { throw new Error('should have caught error') }) .catch((err) => { logger.error(err) snapshot( 'no version of Cypress installed 1', normalize(stdout.toString()), ) }) }) it('adds --no-sandbox when user is root', () => { // make it think the executable exists createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) process.geteuid.returns(0) // user is root util.exec.resolves({ stdout: '222', stderr: '', }) return verify.start() .then(() => { expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) }) }) it('adds --no-sandbox when user is non-root', () => { // make it think the executable exists createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) process.geteuid.returns(1000) // user is non-root util.exec.resolves({ stdout: '222', stderr: '', }) return verify.start() .then(() => { expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) }) }) it('is noop when binary is already verified', () => { // make it think the executable exists and is verified createfs({ alreadyVerified: true, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) return verify.start().then(() => { // nothing should have been logged to stdout // since no verification took place expect(stdout.toString()).to.be.empty expect(util.exec).not.to.be.called }) }) it('logs warning when installed version does not match verified version', () => { createfs({ alreadyVerified: true, executable: mockfs.file({ mode: 0o777 }), packageVersion: 'bloop', }) return verify .start() .then(() => { throw new Error('should have caught error') }) .catch(() => { return snapshot( 'warning installed version does not match verified version 1', normalize(stdout.toString()), ) }) }) it('logs error and exits when executable cannot be found', () => { return verify .start() .then(() => { throw new Error('should have caught error') }) .catch((err) => { logger.error(err) snapshot('executable cannot be found 1', normalize(stdout.toString())) }) }) it('logs error when child process hangs', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) sinon.stub(cp, 'spawn').callsFake(mockSpawn((cp) => { cp.stderr.write('some stderr') cp.stdout.write('some stdout') })) util.exec.restore() return verify .start({ smokeTestTimeout: 1 }) .catch((err) => { logger.error(err) }) .then(() => { snapshot(normalize(slice(stdout.toString()))) }) }) it('logs error when child process returns incorrect stdout (stderr when exists)', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) sinon.stub(cp, 'spawn').callsFake(mockSpawn((cp) => { cp.stderr.write('some stderr') cp.stdout.write('some stdout') cp.emit('exit', 0, null) cp.end() })) util.exec.restore() return verify .start() .catch((err) => { logger.error(err) }) .then(() => { snapshot(normalize(slice(stdout.toString()))) }) }) it('logs error when child process returns incorrect stdout (stdout when no stderr)', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) sinon.stub(cp, 'spawn').callsFake(mockSpawn((cp) => { cp.stdout.write('some stdout') cp.emit('exit', 0, null) cp.end() })) util.exec.restore() return verify .start() .catch((err) => { logger.error(err) }) .then(() => { snapshot(normalize(slice(stdout.toString()))) }) }) it('sets ELECTRON_ENABLE_LOGGING without mutating process.env', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined util.exec.resolves() sinon.stub(util, 'stdoutLineMatches').returns(true) return verify .start() .then(() => { expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined const stdioOptions = util.exec.firstCall.args[2] expect(stdioOptions).to.include({ timeout: verify.VERIFY_TEST_RUNNER_TIMEOUT_MS, }) expect(stdioOptions.env).to.include({ ELECTRON_ENABLE_LOGGING: true, }) }) }) describe('with force: true', () => { beforeEach(() => { createfs({ alreadyVerified: true, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) }) it('shows full path to executable when verifying', () => { return verify.start({ force: true }).then(() => { snapshot('verification with executable 1', normalize(stdout.toString())) }) }) it('clears verified version from state if verification fails', () => { util.exec.restore() sinon .stub(util, 'exec') .withArgs(executablePath) .rejects({ code: 1, stderr: 'an error about dependencies', }) return verify .start({ force: true }) .then(() => { throw new Error('Should have thrown') }) .catch((err) => { logger.error(err) }) .then(() => { return fs.pathExistsAsync(binaryStatePath) }) .then((exists) => { return expect(exists).to.eq(false) }) .then(() => { return snapshot( 'fails verifying Cypress 1', normalize(slice(stdout.toString())), ) }) }) }) describe('smoke test with DEBUG output', () => { beforeEach(() => { const stdoutWithDebugOutput = stripIndent` some debug output date: more debug output 222 after that more text ` util.exec.withArgs(executablePath).resolves({ stdout: stdoutWithDebugOutput, }) createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) }) it('finds ping value in the verbose output', () => { return verify.start().then(() => { snapshot('verbose stdout output 1', normalize(stdout.toString())) }) }) }) describe('smoke test retries on bad display with our Xvfb', () => { let restore beforeEach(() => { restore = mockedEnv({ DISPLAY: 'test-display', }) createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) util.exec.restore() sinon.spy(logger, 'warn') }) afterEach(() => { restore() }) it('successfully retries with our Xvfb on Linux', () => { // initially we think the user has everything set xvfb.isNeeded.returns(false) sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true) sinon.stub(util, 'exec').callsFake(() => { const firstSpawnError = new Error('') // this message contains typical Gtk error shown if X11 is incorrect // like in the case of DISPLAY=987 firstSpawnError.stderr = stripIndent` [some noise here] Gtk: cannot open display: 987 and maybe a few other lines here with weird indent ` firstSpawnError.stdout = '' // the second time the binary returns expected ping util.exec.withArgs(executablePath).resolves({ stdout: '222', }) return Promise.reject(firstSpawnError) }) return verify.start().then(() => { expect(util.exec).to.have.been.calledTwice // user should have been warned expect(logger.warn).to.have.been.calledWithMatch( 'This is likely due to a misconfigured DISPLAY environment variable.', ) }) }) it('fails on both retries with our Xvfb on Linux', () => { // initially we think the user has everything set xvfb.isNeeded.returns(false) sinon.stub(util, 'isPossibleLinuxWithIncorrectDisplay').returns(true) sinon.stub(util, 'exec').callsFake(() => { os.platform.returns('linux') expect(xvfb.start).to.not.have.been.called const firstSpawnError = new Error('') // this message contains typical Gtk error shown if X11 is incorrect // like in the case of DISPLAY=987 firstSpawnError.stderr = stripIndent` [some noise here] Gtk: cannot open display: 987 and maybe a few other lines here with weird indent ` firstSpawnError.stdout = '' // the second time it runs, it fails for some other reason const secondMessage = stripIndent` [some noise here] Gtk: cannot open display: 987 some other error again with some weird indent ` util.exec.withArgs(executablePath).rejects(new Error(secondMessage)) return Promise.reject(firstSpawnError) }) return verify.start().then(() => { throw new Error('Should have failed') }) .catch((e) => { expect(util.exec).to.have.been.calledTwice // second time around we should have called Xvfb expect(xvfb.start).to.have.been.calledOnce expect(xvfb.stop).to.have.been.calledOnce // user should have been warned expect(logger.warn).to.have.been.calledWithMatch('DISPLAY was set to: "test-display"') snapshot('tried to verify twice, on the first try got the DISPLAY error', e.message) }) }) }) it('logs an error if Cypress executable does not exist', () => { createfs({ alreadyVerified: false, executable: false, packageVersion, }) return verify .start() .then(() => { throw new Error('Should have thrown') }) .catch((err) => { stdout = Stdout.capture() logger.error(err) return snapshot('no Cypress executable 1', normalize(stdout.toString())) }) }) it('logs an error if Cypress executable does not have permissions', () => { mockfs.restore() createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o666 }), packageVersion, }) return verify .start() .then(() => { throw new Error('Should have thrown') }) .catch((err) => { stdout = Stdout.capture() logger.error(err) return snapshot( 'Cypress non-executable permissions 1', normalize(stdout.toString()), ) }) }) it('logs and runs when current version has not been verified', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) return verify.start().then(() => { return snapshot( 'current version has not been verified 1', normalize(stdout.toString()), ) }) }) it('logs and runs when installed version is different than package version', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion: '7.8.9', }) return verify.start().then(() => { return snapshot( 'different version installed 1', normalize(stdout.toString()), ) }) }) it('is silent when logLevel is silent', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) process.env.npm_config_loglevel = 'silent' return verify.start().then(() => { return snapshot( 'silent verify 1', normalize(`[no output]${stdout.toString()}`), ) }) }) it('turns off Opening Cypress...', () => { createfs({ alreadyVerified: true, executable: mockfs.file({ mode: 0o777 }), packageVersion: '7.8.9', }) return verify .start({ welcomeMessage: false, }) .then(() => { return snapshot('no welcome message 1', normalize(stdout.toString())) }) }) it('logs error when fails smoke test unexpectedly without stderr', () => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) util.exec.restore() sinon.stub(util, 'exec').rejects({ stderr: '', stdout: '', message: 'Error: EPERM NOT PERMITTED', }) return verify .start() .then(() => { throw new Error('Should have thrown') }) .catch((err) => { stdout = Stdout.capture() logger.error(err) return snapshot('fails with no stderr 1', normalize(stdout.toString())) }) }) describe('on linux', () => { beforeEach(() => { xvfb.isNeeded.returns(true) createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) }) it('starts xvfb', () => { return verify.start().then(() => { expect(xvfb.start).to.be.called }) }) it('stops xvfb on spawned process close', () => { return verify.start().then(() => { expect(xvfb.stop).to.be.called }) }) it('logs error and exits when starting xvfb fails', () => { const err = new Error('test without xvfb') xvfb.start.restore() err.nonZeroExitCode = true err.stack = 'xvfb? no dice' sinon.stub(xvfb._xvfb, 'startAsync').rejects(err) return verify.start() .then(() => { throw new Error('should have thrown') }) .catch((err) => { expect(xvfb.stop).to.be.calledOnce logger.error(err) snapshot('xvfb fails 1', normalize(slice(stdout.toString()))) }) }) }) describe('when running in CI', () => { beforeEach(() => { createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, }) util.isCi.returns(true) }) it('uses verbose renderer', () => { return verify.start().then(() => { snapshot('verifying in ci 1', normalize(stdout.toString())) }) }) it('logs error when binary not found', () => { mockfs({}) return verify .start() .then(() => { throw new Error('Should have thrown') }) .catch((err) => { logger.error(err) snapshot('error binary not found in ci 1', normalize(stdout.toString())) }) }) }) describe('when env var CYPRESS_RUN_BINARY', () => { it('can validate and use executable', () => { const envBinaryPath = '/custom/Contents/MacOS/Cypress' const realEnvBinaryPath = `/real${envBinaryPath}` process.env.CYPRESS_RUN_BINARY = envBinaryPath createfs({ alreadyVerified: false, executable: mockfs.file({ mode: 0o777 }), packageVersion, customDir: '/real/custom', }) util.exec .withArgs(realEnvBinaryPath, ['--no-sandbox', '--smoke-test', '--ping=222']) .resolves(spawnedProcess) return verify.start().then(() => { expect(util.exec.firstCall.args[0]).to.equal(realEnvBinaryPath) snapshot('valid CYPRESS_RUN_BINARY 1', normalize(stdout.toString())) }) }) _.each(['darwin', 'linux', 'win32'], (platform) => { return it('can log error to user', () => { process.env.CYPRESS_RUN_BINARY = '/custom/' os.platform.returns(platform) return verify .start() .then(() => { throw new Error('Should have thrown') }) .catch((err) => { logger.error(err) snapshot( `${platform}: error when invalid CYPRESS_RUN_BINARY 1`, normalize(stdout.toString()), ) }) }) }) }) // tests for when Electron needs "--no-sandbox" CLI flag context('.needsSandbox', () => { it('needs --no-sandbox on Linux as a root', () => { os.platform.returns('linux') process.geteuid.returns(0) // user is root expect(verify.needsSandbox()).to.be.true }) it('needs --no-sandbox on Linux as a non-root', () => { os.platform.returns('linux') process.geteuid.returns(1000) // user is non-root expect(verify.needsSandbox()).to.be.true }) it('needs --no-sandbox on Mac as a non-root', () => { os.platform.returns('darwin') process.geteuid.returns(1000) // user is non-root expect(verify.needsSandbox()).to.be.true }) it('does not need --no-sandbox on Windows', () => { os.platform.returns('win32') expect(verify.needsSandbox()).to.be.false }) }) }) // TODO this needs documentation with examples badly. function createfs ({ alreadyVerified, executable, packageVersion, customDir }) { if (!customDir) { customDir = '/cache/Cypress/1.2.3/Cypress.app' } // binary state is stored one folder higher than the runner itself // see https://github.com/cypress-io/cypress/issues/6089 const binaryStateFolder = path.join(customDir, '..') const binaryState = { verified: alreadyVerified, } const binaryStateText = JSON.stringify(binaryState) let mockFiles = { [binaryStateFolder]: { 'binary_state.json': binaryStateText, }, [customDir]: { Contents: { MacOS: executable ? { Cypress: executable, } : {}, Resources: { app: { 'package.json': `{"version": "${packageVersion}"}`, }, }, }, }, } if (customDir) { mockFiles['/custom/Contents/MacOS/Cypress'] = mockfs.symlink({ path: '/real/custom/Contents/MacOS/Cypress', mode: 0o777, }) } return mockfs(mockFiles) } function slice (str) { // strip answer and split by new lines str = str.split('\n') // find the line about verifying cypress can run const index = _.findIndex(str, (line) => { return line.includes('Verifying Cypress can run') }) // get rid of whatever the next line is because // i cannot figure out why this line fails in CI // its likely due to some UTF code str.splice(index + 1, 1, 'STRIPPED') return str.join('\n') }