Files
cypress/cli/test/lib/util.spec.ts
Bill Glesias 91fe08e57e chore: update CLI snapshots to include ANSI, replace check-more-types and la, and use vi.mocked (#32486)
* chore: rework vitest mocks

* chore: turn on ANSI for snapshots in the CLI

* chore: remove check-more-types from the CLI and just use lodash/regex

* chore: remove lazy-ass in favor of the native assertion library in node
2025-09-15 15:15:52 -04:00

723 lines
19 KiB
TypeScript

import { vi, describe, it, beforeEach, expect } from 'vitest'
import assert from 'assert'
import hasha from 'hasha'
import util from '../../lib/util'
import logger from '../../lib/logger'
import { Systeminformation } from 'systeminformation'
describe('util', () => {
beforeEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
})
describe('.isBrokenGtkDisplay', () => {
it('detects only GTK message', () => {
const text = '[some noise here] Gtk: cannot open display: 99'
expect(util.isBrokenGtkDisplay(text)).toEqual(true)
// and not for the other messages
expect(util.isBrokenGtkDisplay('display was set incorrectly')).toEqual(false)
})
})
describe('.getGitHubIssueUrl', () => {
it('returns url for issue number', () => {
const url = util.getGitHubIssueUrl(4034)
expect(url).toEqual('https://github.com/cypress-io/cypress/issues/4034')
})
it('throws for anything but a positive integer', () => {
// @ts-expect-error - args don't fit type
expect(() => util.getGitHubIssueUrl('4024')).toThrow()
expect(() => util.getGitHubIssueUrl(-5)).toThrow()
expect(() => util.getGitHubIssueUrl(5.19)).toThrow()
})
})
describe('.stdoutLineMatches', () => {
it('is a function', () => {
expect(util.stdoutLineMatches).toBeTypeOf('function')
})
it('matches entire output', () => {
const line = '444'
expect(util.stdoutLineMatches(line, line)).toEqual(true)
})
it('matches a line in output', () => {
const line = '444'
const stdout = ['start', line, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(true)
})
it('matches a trimmed line in output', () => {
const line = '444'
const stdout = ['start', ` ${line} `, 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(true)
})
it('does not find match', () => {
const line = '445'
const stdout = ['start', '444', 'something else'].join('\n')
expect(util.stdoutLineMatches(line, stdout)).toEqual(false)
})
})
describe('.normalizeModuleOptions', () => {
it('does not change other properties', () => {
const options = {
foo: 'bar',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('passes string env unchanged', () => {
const options = {
env: 'foo=bar',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts environment object', () => {
const options = {
env: {
foo: 'bar',
magicNumber: 1234,
host: 'kevin.dev.local',
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts config object', () => {
const options = {
config: {
baseUrl: 'http://localhost:2000',
watchForFileChanges: false,
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts reporterOptions object', () => {
const options = {
reporterOptions: {
mochaFile: 'results/my-test-output.xml',
toConsole: true,
},
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('converts specs array', () => {
const options = {
spec: [
'a', 'b', 'c',
],
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
it('does not convert spec when string', () => {
const options = {
spec: 'x,y,z',
}
expect(util.normalizeModuleOptions(options)).toMatchSnapshot()
})
})
describe('.supportsColor', () => {
beforeEach(() => {
// make sure CI is undefined when running in CircleCI to get deterministic results
vi.stubEnv('CI', undefined)
})
it('is true on obj return for stdout and stderr', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: true,
},
}
})
const utils = (await import('../../lib/util')).default
expect(utils.supportsColor()).toEqual(true)
})
it('is false on false return for stdout', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: false,
stderr: true,
},
}
})
const utils = (await import('../../lib/util')).default
expect(utils.supportsColor()).toEqual(false)
})
it('is false on false return for stderr', async () => {
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: false,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(false)
})
it('is true when running in CI', async () => {
vi.stubEnv('CI', '1')
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: false,
stderr: false,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(true)
})
it('is false when NO_COLOR has been set', async () => {
vi.stubEnv('CI', '1')
vi.stubEnv('NO_COLOR', '1')
vi.doMock('supports-color', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
stdout: true,
stderr: true,
},
}
})
const util = (await import('../../lib/util')).default
expect(util.supportsColor()).toEqual(false)
})
})
describe('.getEnvOverrides', () => {
it('returns object with colors + process overrides', async () => {
// force supportColors to return true
vi.stubEnv('CI', '1')
vi.doMock('tty', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
isatty: vi.fn(),
},
}
})
const tty = (await import('tty')).default
vi.mocked(tty.isatty).mockReturnValue(true)
const util = (await import('../../lib/util')).default
expect(util.getEnvOverrides()).toEqual({
FORCE_STDIN_TTY: '1',
FORCE_STDOUT_TTY: '1',
FORCE_STDERR_TTY: '1',
FORCE_COLOR: '1',
DEBUG_COLORS: '1',
MOCHA_COLORS: '1',
})
// force supportColors to return false
vi.stubEnv('CI', undefined)
vi.stubEnv('NO_COLOR', '1')
vi.mocked(tty.isatty).mockReturnValue(false)
expect(util.getEnvOverrides()).toEqual({
FORCE_STDIN_TTY: '0',
FORCE_STDOUT_TTY: '0',
FORCE_STDERR_TTY: '0',
FORCE_COLOR: '0',
DEBUG_COLORS: '0',
})
})
})
describe('.getForceTty', () => {
it('forces when each stream is a tty', async () => {
vi.doMock('tty', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
isatty: vi.fn(),
},
}
})
const tty = (await import('tty')).default
vi.mocked(tty.isatty).mockImplementation((args: number) => {
if (args === 0 || args === 1 || args === 2) {
return true
}
return false
})
const util = (await import('../../lib/util')).default
expect(util.getForceTty()).toEqual({
FORCE_STDIN_TTY: true,
FORCE_STDOUT_TTY: true,
FORCE_STDERR_TTY: true,
})
vi.mocked(tty.isatty).mockReturnValue(false)
expect(util.getForceTty()).toEqual({
FORCE_STDIN_TTY: false,
FORCE_STDOUT_TTY: false,
FORCE_STDERR_TTY: false,
})
})
})
describe('.getOriginalNodeOptions', () => {
it('copy NODE_OPTIONS to ORIGINAL_NODE_OPTIONS', () => {
vi.stubEnv('NODE_OPTIONS', '--require foo.js')
// @ts-expect-error - bad type
expect(util.getOriginalNodeOptions({})).toEqual({
ORIGINAL_NODE_OPTIONS: '--require foo.js',
})
})
})
describe('.exit', () => {
it('calls process.exit', () => {
const processExitSpy = vi.spyOn(process, 'exit').mockReturnValue(undefined as never)
util.exit(2)
util.exit(0)
expect(processExitSpy).toHaveBeenCalledWith(2)
expect(processExitSpy).toHaveBeenCalledWith(0)
})
})
describe('.logErrorExit1', () => {
it('calls logger.error and process.exit', () => {
const err = new Error('foo')
const processExitSpy = vi.spyOn(process, 'exit').mockReturnValue(undefined as never)
const loggerErrorSpy = vi.spyOn(logger, 'error').mockReturnValue(undefined as never)
util.logErrorExit1(err)
expect(processExitSpy).toHaveBeenCalledWith(1)
expect(loggerErrorSpy).toHaveBeenCalledWith('foo')
})
})
describe('.isSemver', () => {
it('is true with 3-digit version', () => {
expect(util.isSemver('1.2.3')).toEqual(true)
})
it('is true with 2-digit version', () => {
expect(util.isSemver('1.2')).toEqual(true)
})
it('is true with 1-digit version', () => {
expect(util.isSemver('1')).toEqual(true)
})
it('is false with URL', () => {
expect(util.isSemver('www.cypress.io/download/1.2.3')).toEqual(false)
})
it('is false with file path', () => {
expect(util.isSemver('0/path/1.2.3/mypath/2.3')).toEqual(false)
})
})
describe('.calculateEta', () => {
it('Remaining eta is same as elapsed when 50%', () => {
expect(util.calculateEta(50, 1000)).toEqual(1000)
})
it('Remaining eta is 0 when 100%', () => {
expect(util.calculateEta(100, 500)).toEqual(0)
})
})
describe('.convertPercentToPercentage', () => {
it('converts to 100 when 1', () => {
expect(util.convertPercentToPercentage(1)).toEqual(100)
})
it('strips out extra decimals', () => {
expect(util.convertPercentToPercentage(0.37892)).toEqual(38)
})
it('returns 0 if null num', () => {
expect(util.convertPercentToPercentage(null)).toEqual(0)
})
})
describe('.printNodeOptions', () => {
describe('NODE_OPTIONS is not set', () => {
it('does nothing if debug is not enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = false
util.printNodeOptions(log)
expect(log).not.toHaveBeenCalled()
})
it('prints message when debug is enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = true
util.printNodeOptions(log)
expect(log).toHaveBeenCalledWith('NODE_OPTIONS is not set')
})
})
describe('NODE_OPTIONS is set', () => {
beforeEach(() => {
vi.stubEnv('NODE_OPTIONS', 'foo')
})
it('does nothing if debug is not enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = false
util.printNodeOptions(log)
expect(log).not.toHaveBeenCalled()
})
it('prints value when debug is enabled', () => {
const log = vi.fn()
// @ts-expect-error wrong signature for mock
log.enabled = true
util.printNodeOptions(log)
expect(log).toHaveBeenCalledWith('NODE_OPTIONS=%s', 'foo')
})
})
})
describe('.getOsVersionAsync', () => {
beforeEach(() => {
vi.doMock('os', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
platform: vi.fn(),
release: vi.fn(),
},
}
})
vi.doMock('systeminformation', async (importActual) => {
const actual = await importActual()
return {
// @ts-expect-error
...actual,
default: {
osInfo: vi.fn(),
},
}
})
})
it('calls os.release when systeminformation fails', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
vi.mocked(os.release).mockReturnValue('some-release')
vi.mocked(si.osInfo).mockRejectedValue(new Error('systeminformation failed'))
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('some-release')
expect(os.release).toHaveBeenCalled()
expect(si.osInfo).toHaveBeenCalled()
})
it('uses systeminformation when it succeeds', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
vi.mocked(si.osInfo).mockResolvedValue({
distro: 'Ubuntu',
release: '22.04',
} as Systeminformation.OsData)
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('Ubuntu - 22.04')
expect(si.osInfo).toHaveBeenCalled()
expect(os.release).not.toHaveBeenCalled()
})
it('falls back to os.release when systeminformation returns incomplete data', async () => {
const os = (await import('os')).default
const si = (await import('systeminformation')).default
vi.mocked(os.release).mockReturnValue('5.15.0')
vi.mocked(si.osInfo).mockResolvedValue({
distro: 'Ubuntu',
// missing release property
} as Systeminformation.OsData)
const util = (await import('../../lib/util')).default
const result = await util.getOsVersionAsync()
expect(result).toEqual('5.15.0')
expect(si.osInfo).toHaveBeenCalled()
expect(os.release).toHaveBeenCalled()
})
})
describe('dequote', () => {
it('removes double quotes', () => {
expect(util.dequote('"foo"')).toEqual('foo')
})
it('keeps single quotes', () => {
expect(util.dequote('\'foo\'')).toEqual('\'foo\'')
})
it('keeps unbalanced double quotes', () => {
expect(util.dequote('"foo')).toEqual('"foo')
})
it('keeps inner double quotes', () => {
expect(util.dequote('a"b"c')).toEqual('a"b"c')
})
it('passes empty strings', () => {
expect(util.dequote('')).toEqual('')
})
it('keeps single double quote character', () => {
expect(util.dequote('"')).toEqual('"')
})
})
describe('.getEnv', () => {
it('reads from package.json config', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('reads from .npmrc config', () => {
vi.stubEnv('npm_config_CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('reads from env var', () => {
vi.stubEnv('CYPRESS_FOO', 'bar')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('prefers env var over .npmrc config', () => {
vi.stubEnv('CYPRESS_FOO', 'bar')
vi.stubEnv('npm_config_CYPRESS_FOO', 'baz')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bar')
})
it('prefers env var over .npmrc config even if it\'s an empty string', () => {
vi.stubEnv('CYPRESS_FOO', '')
vi.stubEnv('npm_config_CYPRESS_FOO', 'baz')
expect(util.getEnv('CYPRESS_FOO')).toEqual('')
})
it('prefers .npmrc config over package config', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'baz')
vi.stubEnv('npm_config_CYPRESS_FOO', 'bloop')
expect(util.getEnv('CYPRESS_FOO')).toEqual('bloop')
})
it('prefers .npmrc config over package config even if it\'s an empty string', () => {
vi.stubEnv('npm_package_config_CYPRESS_FOO', 'baz')
vi.stubEnv('npm_config_CYPRESS_FOO', '')
expect(util.getEnv('CYPRESS_FOO')).toEqual('')
})
it('npm config set should work', () => {
vi.stubEnv('npm_config_cypress_foo_foo', 'bazz')
expect(util.getEnv('CYPRESS_FOO_FOO')).toEqual('bazz')
})
it('throws on non-string name', () => {
// @ts-expect-error - args don't fit type
expect(() => util.getEnv()).toThrow()
// @ts-expect-error - args don't fit type
expect(() => util.getEnv(42)).toThrow()
})
describe('with trim = true', () => {
it('trims returned string', () => {
vi.stubEnv('FOO', ' bar ')
expect(util.getEnv('FOO', true)).toEqual('bar')
})
it('removes quotes from the returned string', () => {
vi.stubEnv('FOO', ' "bar" ')
expect(util.getEnv('FOO', true)).toEqual('bar')
})
it('removes only single level of double quotes', () => {
vi.stubEnv('FOO', ' ""bar"" ')
expect(util.getEnv('FOO', true)).toEqual('"bar"')
})
it('keeps unbalanced double quote', () => {
vi.stubEnv('FOO', ' "bar ')
expect(util.getEnv('FOO', true)).toEqual('"bar')
})
it('trims but does not remove single quotes', () => {
vi.stubEnv('FOO', ' \'bar\' ')
expect(util.getEnv('FOO', true)).toEqual('\'bar\'')
})
it('keeps whitespace inside removed quotes', () => {
vi.stubEnv('FOO', '"foo.txt "')
expect(util.getEnv('FOO', true)).toEqual('foo.txt ')
})
})
})
describe('.getFileChecksum', () => {
it('computes same hash as Hasha SHA512', async () => {
const [checksum, expectedChecksum] = await Promise.all([
util.getFileChecksum(__filename),
hasha.fromFile(__filename, { algorithm: 'sha512' }),
])
assert.ok(checksum === expectedChecksum, `checksum ${checksum} is different from expected "${expectedChecksum}"`)
})
})
describe('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).toEqual({
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).toEqual({
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).toEqual({
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).toEqual({
ciBuildId: 'my ci build id',
})
})
})
})