Files
cypress/cli/test/lib/tasks/install_spec.ts
Bill Glesias 3481d1acaf chore: refactor cypress/cli to TypeScript (#32063)
* migrate cli scripts to TypeScript

* convert all javascript source files in the CLI to TypeScript

rebase into first

* chore: refactor all tests to TypeScript

rebase into second

* add npmignore for cli for typescript files

* update build process

* fix publically available exports

* Fix cy-in-cy tests

* add ts-expect-error to failing files

* fix projectConfigIpc failures as there are now multiple installs of tsx

* fix after-pack hook

* fix binary script

* chore: update publish binary to account for CLI being an ESModule compiled down to CommonJS

* does this work?

* fix the verify spec by making the listr2 renderer silent as it behaves differently since the refactor and is printing non deterministic outputs into our tests that do not have a large impact on the area we are testing and mostly served to actually test the renders of the listr2 framework itself

* empty commit

* additional refactor to code to remove strange any typing and exporting

* bump cache and build binaries

* fix CLI exports to keep backwards compatibility

* fix unit-tests

* turn on mac jobs

* fix group name rename in CLI

* remove babel deps from cli and explicitly install typescript

* address feedback from code review

* dont just falsy check results and instead explicitly check for null or undefined

* add ts-expect-error

* additional pass on cleaning up dynamic require / import from global lib references

* annotate ts-expect-errors with reason for why error is expected

* add rest of ts-expect-error comments

* removing hardcoded branch to publish binary chore/migrate_cli_to_typescript
2025-09-02 17:52:45 -04:00

521 lines
16 KiB
TypeScript

import '../../spec_helper'
import os from 'os'
import path from 'path'
import chalk from 'chalk'
import BluebirdPromise from 'bluebird'
import mockfs from 'mock-fs'
import snapshot from '../../support/snapshot'
import stdout from '../../support/stdout'
import normalize from '../../support/normalize'
import fs from '../../../lib/fs'
import logger from '../../../lib/logger'
import util from '../../../lib/util'
import download from '../../../lib/tasks/download'
import unzip from '../../../lib/tasks/unzip'
import install from '../../../lib/tasks/install'
import state from '../../../lib/tasks/state'
const packageVersion = '1.2.3'
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`)
const installDir = '/cache/Cypress/1.2.3'
describe('/lib/tasks/install', function () {
before(async function () {
const mochaMain = await import('mocha-banner')
mochaMain.register()
})
beforeEach(function () {
(this as any).stdout = stdout.capture()
// allow simpler log message comparison without
// chalk's terminal control strings
chalk.level = 0
})
afterEach(() => {
stdout.restore()
chalk.level = 3
})
context('.start', function () {
beforeEach(function () {
logger.reset()
sinon.stub(util, 'isCi').returns(false)
sinon.stub(util, 'isPostInstall').returns(false)
sinon.stub(util, 'pkgVersion').returns(packageVersion)
sinon.stub(download, 'start').resolves(packageVersion)
sinon.stub(unzip, 'start').resolves()
sinon.stub(BluebirdPromise, 'delay').resolves()
sinon.stub(fs, 'removeAsync').resolves()
sinon.stub(state, 'getVersionDir').returns('/cache/Cypress/1.2.3')
sinon.stub(state, 'getBinaryDir').returns('/cache/Cypress/1.2.3/Cypress.app')
sinon.stub(state, 'getBinaryPkgAsync').resolves()
sinon.stub(fs, 'ensureDirAsync').resolves(undefined)
;(os.platform as any).returns('darwin')
})
describe('skips install', function () {
it('when environment variable is set', function () {
process.env.CYPRESS_INSTALL_BINARY = '0'
return install.start()
.then(() => {
expect(download.start).not.to.be.called
snapshot(
'skip installation 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('non-stable builds', () => {
const buildInfo = {
stable: false,
commitSha: '3b7f0b5c59def1e9b5f385bd585c9b2836706c29',
commitBranch: 'aBranchName',
commitDate: new Date('1996-11-27').toISOString(),
}
function runInstall () {
return install.start({ buildInfo })
}
it('install from a constructed CDN URL', async function () {
await runInstall()
expect(download.start).to.be.calledWithMatch({
version: 'https://cdn.cypress.io/beta/binary/0.0.0-development/darwin-x64/aBranchName-3b7f0b5c59def1e9b5f385bd585c9b2836706c29/cypress.zip',
})
})
it('logs a warning about installing a pre-release', async function () {
await runInstall()
snapshot(normalize((this as any).stdout.toString()))
})
it('installs to the expected pre-release cache dir', async function () {
(state.getVersionDir as any).restore()
await runInstall()
expect(unzip.start).to.be.calledWithMatch({ installDir: sinon.match(/\/Cypress\/beta\-1\.2\.3\-aBranchName\-3b7f0b5c$/) })
})
})
describe('override version', function () {
it('warns when specifying cypress version in env', function () {
const version = '0.12.1'
process.env.CYPRESS_INSTALL_BINARY = version
return install.start()
.then(() => {
expect(download.start).to.be.calledWithMatch({
version,
})
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: downloadDestination,
})
snapshot(
'specify version in env vars 1',
normalize((this as any).stdout.toString()),
)
})
})
it('trims environment variable before installing', function () {
// note how the version has extra spaces around it on purpose
const filename = '/tmp/local/file.zip'
const version = ` ${filename} `
process.env.CYPRESS_INSTALL_BINARY = version
// internally, the variable should be trimmed and just filename checked
sinon.stub(fs, 'pathExistsAsync').withArgs(filename).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: filename,
installDir,
})
})
})
it('removes double quotes around the environment variable before installing', function () {
// note how the version has extra spaces around it on purpose
// and there are double quotes
const filename = '/tmp/local/file.zip'
const version = ` "${filename}" `
process.env.CYPRESS_INSTALL_BINARY = version
// internally, the variable should be trimmed, double quotes removed
// and just filename checked against the file system
sinon.stub(fs, 'pathExistsAsync').withArgs(filename).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: filename,
installDir,
})
})
})
it('can install local binary zip file without download from absolute path', function () {
const version = '/tmp/local/file.zip'
process.env.CYPRESS_INSTALL_BINARY = version
sinon.stub(fs, 'pathExistsAsync').withArgs(version).resolves(true)
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: version,
installDir,
})
})
})
it('can install local binary zip file from relative path', function () {
const version = './cypress-resources/file.zip'
mockfs({
[version]: 'asdf',
})
process.env.CYPRESS_INSTALL_BINARY = version
const installDir = state.getVersionDir()
return install.start()
.then(() => {
expect(download.start).not.to.be.called
expect(unzip.start).to.be.calledWithMatch({
zipFilePath: path.resolve(version),
installDir,
})
})
})
describe('when version is already installed', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: packageVersion })
})
it('doesn\'t attempt to download', function () {
return install.start()
.then(() => {
expect(download.start).not.to.be.called
expect(state.getBinaryPkgAsync).to.be.calledWith('/cache/Cypress/1.2.3/Cypress.app')
})
})
it('logs \'skipping install\' when explicit cypress install', function () {
return install.start()
.then(() => {
return snapshot(
'version already installed - cypress install 1',
normalize((this as any).stdout.toString()),
)
})
})
it('logs when already installed when run from postInstall', function () {
(util.isPostInstall as any).returns(true)
return install.start()
.then(() => {
snapshot(
'version already installed - postInstall 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('when getting installed version fails', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves(null)
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'continues installing on failure 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when there is no install version', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves(null)
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
// cleans up the zip file
expect(fs.removeAsync).to.be.calledWith(
downloadDestination,
)
snapshot(
'installs without existing installation 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when getting installed version does not match needed version', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'installed version does not match needed version 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('with force: true', function () {
beforeEach(function () {
(state.getBinaryPkgAsync as any).resolves({ version: packageVersion })
return install.start({ force: true })
})
it('logs message and starts download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'forcing true always installs 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('as a global install', function () {
beforeEach(function () {
sinon.stub(util, 'isInstalledGlobally').returns(true)
;(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('logs global warning and download', function () {
expect(download.start).to.be.calledWithMatch({
version: packageVersion,
})
expect(unzip.start).to.be.calledWithMatch({
installDir,
})
snapshot(
'warning installing as global 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('when running in CI', function () {
beforeEach(function () {
(util.isCi as any).returns(true)
;(state.getBinaryPkgAsync as any).resolves({ version: 'x.x.x' })
return install.start()
})
it('uses verbose renderer', function () {
snapshot(
'installing in ci 1',
normalize((this as any).stdout.toString()),
)
})
})
describe('failed write access to cache directory', function () {
it('logs error on failure', function () {
(os.platform as any).returns('darwin')
sinon.stub(state, 'getCacheDir').returns('/invalid/cache/dir')
const err: any = new Error('EACCES: permission denied, mkdir \'/invalid\'')
err.code = 'EACCES'
;(fs.ensureDirAsync as any).rejects(err)
return install.start()
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot(
'invalid cache directory 1',
normalize((this as any).stdout.toString()),
)
})
})
})
describe('CYPRESS_INSTALL_BINARY is URL or Zip', function () {
it('uses cache when correct version installed given URL', function () {
(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('1.2.3')
process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5'
return install.start()
.then(() => {
expect(download.start).to.not.be.called
})
})
it('uses cache when mismatch version given URL ', function () {
(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('4.0.0')
process.env.CYPRESS_INSTALL_BINARY = 'www.cypress.io/cannot-download/2.4.5'
return install.start()
.then(() => {
expect(download.start).to.not.be.called
})
})
it('uses cache when correct version installed given Zip', function () {
sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true)
;(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('1.2.3')
process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip'
return install.start()
.then(() => {
expect(unzip.start).to.not.be.called
})
})
it('uses cache when mismatch version given Zip ', function () {
sinon.stub(fs, 'pathExistsAsync').withArgs('/path/to/zip.zip').resolves(true)
;(state.getBinaryPkgAsync as any).resolves({ version: '1.2.3' })
;(util.pkgVersion as any).returns('4.0.0')
process.env.CYPRESS_INSTALL_BINARY = '/path/to/zip.zip'
return install.start()
.then(() => {
expect(unzip.start).to.not.be.called
})
})
})
})
it('is silent when log level is silent', function () {
process.env.npm_config_loglevel = 'silent'
return install.start()
.then(() => {
return snapshot(
'silent install 1',
normalize(`[no output]${(this as any).stdout.toString()}`),
)
})
})
it('exits with error when installing on unsupported os', function () {
sinon.stub(util, 'getPlatformInfo').resolves('Platform: win32-ia32')
return install.start()
.then(() => {
throw new Error('should have caught error')
})
.catch((err: any) => {
logger.error(err)
snapshot(
'error when installing on unsupported os',
normalize((this as any).stdout.toString()),
)
})
})
})
context('._getBinaryUrlFromBuildInfo', function () {
const buildInfo = {
commitSha: 'abc123',
commitBranch: 'aBranchName',
}
it('generates the expected URL', () => {
(os.platform as any).returns('linux')
expect(install._getBinaryUrlFromBuildInfo('x64', buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/linux-x64/aBranchName-abc123/cypress.zip`)
})
it('overrides win32-arm64 to win32-x64 for pre-release', () => {
(os.platform as any).returns('win32')
expect(install._getBinaryUrlFromBuildInfo('arm64', buildInfo))
.to.eq(`https://cdn.cypress.io/beta/binary/0.0.0-development/win32-x64/aBranchName-abc123/cypress.zip`)
})
})
})