require('../spec_helper') const os = require('os') const tty = require('tty') const snapshot = require('../support/snapshot') const mockedEnv = require('mocked-env') const supportsColor = require('supports-color') const proxyquire = require('proxyquire') const hasha = require('hasha') const la = require('lazy-ass') const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) // https://github.com/cypress-io/cypress/issues/5431 const expectedNodeOptions = `--max-http-header-size=${1024 * 1024}` describe('util', () => { beforeEach(() => { sinon.stub(process, 'exit') sinon.stub(logger, 'error') }) context('.isBrokenGtkDisplay', () => { it('detects only GTK message', () => { os.platform.returns('linux') const text = '[some noise here] Gtk: cannot open display: 99' expect(util.isBrokenGtkDisplay(text)).to.be.true // and not for the other messages expect(util.isBrokenGtkDisplay('display was set incorrectly')).to.be.false }) }) context('.getGitHubIssueUrl', () => { it('returls url for issue number', () => { const url = util.getGitHubIssueUrl(4034) expect(url).to.equal('https://github.com/cypress-io/cypress/issues/4034') }) it('throws for anything but a positive integer', () => { expect(() => { return util.getGitHubIssueUrl('4034') }).to.throw expect(() => { return util.getGitHubIssueUrl(-5) }).to.throw expect(() => { return util.getGitHubIssueUrl(5.19) }).to.throw }) }) context('.stdoutLineMatches', () => { const { stdoutLineMatches } = util it('is a function', () => { expect(stdoutLineMatches).to.be.a.function }) it('matches entire output', () => { const line = '444' expect(stdoutLineMatches(line, line)).to.be.true }) it('matches a line in output', () => { const line = '444' const stdout = ['start', line, 'something else'].join('\n') expect(stdoutLineMatches(line, stdout)).to.be.true }) it('matches a trimmed line in output', () => { const line = '444' const stdout = ['start', ` ${line} `, 'something else'].join('\n') expect(stdoutLineMatches(line, stdout)).to.be.true }) it('does not find match', () => { const line = '445' const stdout = ['start', '444', 'something else'].join('\n') expect(stdoutLineMatches(line, stdout)).to.be.false }) }) context('.normalizeModuleOptions', () => { const { normalizeModuleOptions } = util it('does not change other properties', () => { const options = { foo: 'bar', } snapshot('others_unchanged 1', normalizeModuleOptions(options)) }) it('passes string env unchanged', () => { const options = { env: 'foo=bar', } snapshot('env_as_string 1', normalizeModuleOptions(options)) }) it('converts environment object', () => { const options = { env: { foo: 'bar', magicNumber: 1234, host: 'kevin.dev.local', }, } snapshot('env_as_object 1', normalizeModuleOptions(options)) }) it('converts config object', () => { const options = { config: { baseUrl: 'http://localhost:2000', watchForFileChanges: false, }, } snapshot('config_as_object 1', normalizeModuleOptions(options)) }) it('converts reporterOptions object', () => { const options = { reporterOptions: { mochaFile: 'results/my-test-output.xml', toConsole: true, }, } snapshot('reporter_options_as_object 1', normalizeModuleOptions(options)) }) it('converts specs array', () => { const options = { spec: [ 'a', 'b', 'c', ], } snapshot('spec_as_array 1', normalizeModuleOptions(options)) }) it('does not convert spec when string', () => { const options = { spec: 'x,y,z', } snapshot('spec_as_string 1', normalizeModuleOptions(options)) }) }) context('.supportsColor', () => { it('is true on obj return for stdout and stderr', () => { sinon.stub(supportsColor, 'stdout').value({}) sinon.stub(supportsColor, 'stderr').value({}) expect(util.supportsColor()).to.be.true }) it('is false on false return for stdout', () => { delete process.env.CI sinon.stub(supportsColor, 'stdout').value(false) sinon.stub(supportsColor, 'stderr').value({}) expect(util.supportsColor()).to.be.false }) it('is false on false return for stderr', () => { delete process.env.CI sinon.stub(supportsColor, 'stdout').value({}) sinon.stub(supportsColor, 'stderr').value(false) expect(util.supportsColor()).to.be.false }) it('is true when running in CI', () => { process.env.CI = '1' sinon.stub(supportsColor, 'stdout').value(false) expect(util.supportsColor()).to.be.true }) it('is false when NO_COLOR has been set', () => { process.env.CI = '1' process.env.NO_COLOR = '1' sinon.stub(supportsColor, 'stdout').value({}) sinon.stub(supportsColor, 'stderr').value({}) expect(util.supportsColor()).to.be.FALSE }) }) context('.getEnvOverrides', () => { it('returns object with colors + process overrides', () => { // shouldn't be stubbing 'what we own' but its easiest in this case sinon.stub(util, 'supportsColor').returns(true) sinon.stub(tty, 'isatty').returns(true) expect(util.getEnvOverrides()).to.deep.eq({ FORCE_STDIN_TTY: '1', FORCE_STDOUT_TTY: '1', FORCE_STDERR_TTY: '1', FORCE_COLOR: '1', DEBUG_COLORS: '1', MOCHA_COLORS: '1', NODE_OPTIONS: expectedNodeOptions, }) util.supportsColor.returns(false) tty.isatty.returns(false) expect(util.getEnvOverrides()).to.deep.eq({ FORCE_STDIN_TTY: '0', FORCE_STDOUT_TTY: '0', FORCE_STDERR_TTY: '0', FORCE_COLOR: '0', DEBUG_COLORS: '0', NODE_OPTIONS: expectedNodeOptions, }) }) }) context('.getNodeOptions', () => { let restoreEnv afterEach(() => { if (restoreEnv) { restoreEnv() restoreEnv = null } }) it('adds required NODE_OPTIONS', () => { restoreEnv = mockedEnv({ NODE_OPTIONS: undefined, }) expect(util.getNodeOptions({})).to.deep.eq({ NODE_OPTIONS: expectedNodeOptions, }) }) it('includes existing NODE_OPTIONS', () => { restoreEnv = mockedEnv({ NODE_OPTIONS: '--foo --bar', }) expect(util.getNodeOptions({})).to.deep.eq({ NODE_OPTIONS: `${expectedNodeOptions} --foo --bar`, ORIGINAL_NODE_OPTIONS: '--foo --bar', }) }) it('does not return if dev is set and version < 12', () => { expect(util.getNodeOptions({ dev: true, }, 11)).to.be.undefined }) }) context('.getForceTty', () => { it('forces when each stream is a tty', () => { sinon.stub(tty, 'isatty') .withArgs(0).returns(true) .withArgs(1).returns(true) .withArgs(2).returns(true) expect(util.getForceTty()).to.deep.eq({ FORCE_STDIN_TTY: true, FORCE_STDOUT_TTY: true, FORCE_STDERR_TTY: true, }) tty.isatty .withArgs(0).returns(false) .withArgs(1).returns(false) .withArgs(2).returns(false) expect(util.getForceTty()).to.deep.eq({ FORCE_STDIN_TTY: false, FORCE_STDOUT_TTY: false, FORCE_STDERR_TTY: false, }) }) }) context('.exit', () => { it('calls process.exit', () => { process.exit.withArgs(2).withArgs(0) util.exit(2) util.exit(0) }) }) context('.logErrorExit1', () => { it('calls logger.error and process.exit', () => { const err = new Error('foo') logger.error.withArgs('foo') process.exit.withArgs(1) util.logErrorExit1(err) }) }) describe('.isSemver', () => { it('is true with 3-digit version', () => { expect(util.isSemver('1.2.3')).to.equal(true) }) it('is true with 2-digit version', () => { expect(util.isSemver('1.2')).to.equal(true) }) it('is true with 1-digit version', () => { expect(util.isSemver('1')).to.equal(true) }) it('is false with URL', () => { expect(util.isSemver('www.cypress.io/download/1.2.3')).to.equal(false) }) it('is false with file path', () => { expect(util.isSemver('0/path/1.2.3/mypath/2.3')).to.equal(false) }) }) describe('.calculateEta', () => { it('Remaining eta is same as elapsed when 50%', () => { expect(util.calculateEta('50', 1000)).to.equal(1000) }) it('Remaining eta is 0 when 100%', () => { expect(util.calculateEta('100', 500)).to.equal(0) }) }) describe('.convertPercentToPercentage', () => { it('converts to 100 when 1', () => { expect(util.convertPercentToPercentage(1)).to.equal(100) }) it('strips out extra decimals', () => { expect(util.convertPercentToPercentage(0.37892)).to.equal(38) }) it('returns 0 if null num', () => { expect(util.convertPercentToPercentage(null)).to.equal(0) }) }) context('.printNodeOptions', () => { describe('NODE_OPTIONS is not set', () => { it('does nothing if debug is not enabled', () => { const log = sinon.spy() log.enabled = false util.printNodeOptions(log) expect(log).not.have.been.called }) it('prints message when debug is enabled', () => { const log = sinon.spy() log.enabled = true util.printNodeOptions(log) expect(log).to.be.calledWith('NODE_OPTIONS is not set') }) }) describe('NODE_OPTIONS is set', () => { beforeEach(() => { process.env.NODE_OPTIONS = 'foo' }) it('does nothing if debug is not enabled', () => { const log = sinon.spy() log.enabled = false util.printNodeOptions(log) expect(log).not.have.been.called }) it('prints value when debug is enabled', () => { const log = sinon.spy() log.enabled = true util.printNodeOptions(log) expect(log).to.be.calledWith('NODE_OPTIONS=%s', 'foo') }) }) }) describe('.getOsVersionAsync', () => { let util let getos = sinon.stub().resolves(['distro-release']) beforeEach(() => { util = proxyquire(`${lib}/util`, { getos }) }) it('calls os.release on non-linux', () => { os.platform.returns('darwin') os.release.returns('some-release') util.getOsVersionAsync() .then(() => { expect(os.release).to.be.called expect(getos).to.not.be.called }) }) it('NOT calls os.release on linux', () => { os.platform.returns('linux') util.getOsVersionAsync() .then(() => { expect(os.release).to.not.be.called expect(getos).to.be.called }) }) }) describe('dequote', () => { it('removes double quotes', () => { expect(util.dequote('"foo"')).to.equal('foo') }) it('keeps single quotes', () => { expect(util.dequote('\'foo\'')).to.equal('\'foo\'') }) it('keeps unbalanced double quotes', () => { expect(util.dequote('"foo')).to.equal('"foo') }) it('keeps inner double quotes', () => { expect(util.dequote('a"b"c')).to.equal('a"b"c') }) it('passes empty strings', () => { expect(util.dequote('')).to.equal('') }) it('keeps single double quote character', () => { expect(util.dequote('"')).to.equal('"') }) }) describe('.getEnv', () => { it('reads from package.json config', () => { process.env.npm_package_config_CYPRESS_FOO = 'bar' expect(util.getEnv('CYPRESS_FOO')).to.eql('bar') }) it('reads from .npmrc config', () => { process.env.npm_config_CYPRESS_FOO = 'bar' expect(util.getEnv('CYPRESS_FOO')).to.eql('bar') }) it('reads from env var', () => { process.env.CYPRESS_FOO = 'bar' expect(util.getEnv('CYPRESS_FOO')).to.eql('bar') }) it('prefers env var over .npmrc config', () => { process.env.CYPRESS_FOO = 'bar' process.env.npm_config_CYPRESS_FOO = 'baz' expect(util.getEnv('CYPRESS_FOO')).to.eql('bar') }) it('prefers env var over .npmrc config even if it\'s an empty string', () => { process.env.CYPRESS_FOO = '' process.env.npm_config_CYPRESS_FOO = 'baz' expect(util.getEnv('CYPRESS_FOO')).to.eql('') }) it('prefers .npmrc config over package config', () => { process.env.npm_package_config_CYPRESS_FOO = 'baz' process.env.npm_config_CYPRESS_FOO = 'bloop' expect(util.getEnv('CYPRESS_FOO')).to.eql('bloop') }) it('prefers .npmrc config over package config even if it\'s an empty string', () => { process.env.npm_package_config_CYPRESS_FOO = 'baz' process.env.npm_config_CYPRESS_FOO = '' expect(util.getEnv('CYPRESS_FOO')).to.eql('') }) it('throws on non-string name', () => { expect(() => { util.getEnv() }).to.throw() expect(() => { util.getEnv(42) }).to.throw() }) context('with trim = true', () => { it('trims returned string', () => { process.env.FOO = ' bar ' expect(util.getEnv('FOO', true)).to.equal('bar') }) it('removes quotes from the returned string', () => { process.env.FOO = ' "bar" ' expect(util.getEnv('FOO', true)).to.equal('bar') }) it('removes only single level of double quotes', () => { process.env.FOO = ' ""bar"" ' expect(util.getEnv('FOO', true)).to.equal('"bar"') }) it('keeps unbalanced double quote', () => { process.env.FOO = ' "bar ' expect(util.getEnv('FOO', true)).to.equal('"bar') }) it('trims but does not remove single quotes', () => { process.env.FOO = ' \'bar\' ' expect(util.getEnv('FOO', true)).to.equal('\'bar\'') }) it('keeps whitespace inside removed quotes', () => { process.env.FOO = '"foo.txt "' expect(util.getEnv('FOO', true)).to.equal('foo.txt ') }) }) }) context('.getFileChecksum', () => { it('computes same hash as Hasha SHA512', () => { return Promise.all([ util.getFileChecksum(__filename), hasha.fromFile(__filename, { algorithm: 'sha512' }), ]).then(([checksum, expectedChecksum]) => { la(checksum === expectedChecksum, 'our computed checksum', checksum, 'is different from expected', expectedChecksum) }) }) }) context('parseOpts', () => { it('passes normal options and strips unknown ones', () => { const result = util.parseOpts({ unknownOptions: true, group: 'my group name', ciBuildId: 'my ci build id', }) expect(result).to.deep.equal({ group: 'my group name', ciBuildId: 'my ci build id', }) }) it('removes leftover double quotes', () => { const result = util.parseOpts({ group: '"my group name"', ciBuildId: '"my ci build id"', }) expect(result).to.deep.equal({ group: 'my group name', ciBuildId: 'my ci build id', }) }) it('leaves unbalanced double quotes', () => { const result = util.parseOpts({ group: 'my group name"', ciBuildId: '"my ci build id', }) expect(result).to.deep.equal({ group: 'my group name"', ciBuildId: '"my ci build id', }) }) it('works with unspecified options', () => { const result = util.parseOpts({ // notice that "group" option is missing ciBuildId: '"my ci build id"', }) expect(result).to.deep.equal({ ciBuildId: 'my ci build id', }) }) }) })