cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
1,311 lines (1,285 loc) • 75 kB
JavaScript
'use strict';
var xvfb = require('./xvfb-D-nbLwPu.js');
var _ = require('lodash');
var commander = require('commander');
var commonTags = require('common-tags');
var logSymbols = require('log-symbols');
var Debug = require('debug');
var fs = require('fs-extra');
var path = require('path');
var Table = require('cli-table3');
var dayjs = require('dayjs');
var relativeTime = require('dayjs/plugin/relativeTime');
var chalk = require('chalk');
var Bluebird = require('bluebird');
var spawn = require('./spawn-Cbp8Y4I3.js');
var os = require('os');
var listr2 = require('listr2');
var timers = require('timers/promises');
var promises = require('fs/promises');
var assert = require('assert');
var request = require('@cypress/request');
var requestProgress = require('request-progress');
var proxyFromEnv = require('proxy-from-env');
var cp = require('child_process');
var yauzl = require('yauzl');
var extract = require('extract-zip');
var readline = require('readline');
var prettyBytes = require('pretty-bytes');
const debug$6 = Debug('cypress:cli');
const defaultBaseUrl = 'https://download.cypress.io/';
const defaultMaxRedirects = 10;
const getProxyForUrlWithNpmConfig = (url) => {
return proxyFromEnv.getProxyForUrl(url) ||
process.env.npm_config_https_proxy ||
process.env.npm_config_proxy ||
null;
};
const getBaseUrl = () => {
if (xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR')) {
let baseUrl = xvfb.util.getEnv('CYPRESS_DOWNLOAD_MIRROR');
if (!(baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.endsWith('/'))) {
baseUrl += '/';
}
return baseUrl || defaultBaseUrl;
}
return defaultBaseUrl;
};
const getCA = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
if (process.env.npm_config_cafile) {
try {
const caFileContent = yield fs.readFile(process.env.npm_config_cafile, 'utf8');
return caFileContent;
}
catch (error) {
debug$6('error reading ca file', error);
return;
}
}
if (process.env.npm_config_ca) {
return process.env.npm_config_ca;
}
return;
});
const prepend = (arch, urlPath, version) => {
const endpoint = new URL(urlPath, getBaseUrl()).toString();
const platform = os.platform();
const pathTemplate = xvfb.util.getEnv('CYPRESS_DOWNLOAD_PATH_TEMPLATE', true);
if ((platform === 'win32') && (arch === 'arm64')) {
debug$6(`detected platform ${platform} architecture ${arch} combination`);
arch = 'x64';
debug$6(`overriding to download ${platform}-${arch} instead`);
}
return pathTemplate
? (pathTemplate
.replace(/\\?\$\{endpoint\}/g, endpoint)
.replace(/\\?\$\{platform\}/g, platform)
.replace(/\\?\$\{arch\}/g, arch)
.replace(/\\?\$\{version\}/g, version))
: `${endpoint}?platform=${platform}&arch=${arch}`;
};
const getUrl = (arch, version) => {
if (_.isString(version) && version.match(/^https?:\/\/.*$/)) {
debug$6('version is already an url', version);
return version;
}
const urlPath = version ? `desktop/${version}` : 'desktop';
return prepend(arch, urlPath, version || '');
};
const statusMessage = (err) => {
return (err.statusCode
? [err.statusCode, err.statusMessage].join(' - ')
: err.toString());
};
const prettyDownloadErr = (err, url) => {
const msg = commonTags.stripIndent `
URL: ${url}
${statusMessage(err)}
`;
debug$6(msg);
return xvfb.throwFormErrorText(xvfb.errors.failedDownload)(msg);
};
/**
* Checks checksum and file size for the given file. Allows both
* values or just one of them to be checked.
*/
const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
if (expectedSize && expectedChecksum) {
debug$6('verifying checksum and file size');
return Bluebird.join(xvfb.util.getFileChecksum(filename), xvfb.util.getFileSize(filename), (checksum, filesize) => {
if (checksum === expectedChecksum && filesize === expectedSize) {
debug$6('downloaded file has the expected checksum and size ✅');
return;
}
debug$6('raising error: checksum or file size mismatch');
const text = commonTags.stripIndent `
Corrupted download
Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}
Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`;
debug$6(text);
throw new Error(text);
});
}
if (expectedChecksum) {
debug$6('only checking expected file checksum %d', expectedChecksum);
const checksum = yield xvfb.util.getFileChecksum(filename);
if (checksum === expectedChecksum) {
debug$6('downloaded file has the expected checksum ✅');
return;
}
debug$6('raising error: file checksum mismatch');
const text = commonTags.stripIndent `
Corrupted download
Expected downloaded file to have checksum: ${expectedChecksum}
Computed checksum: ${checksum}
`;
throw new Error(text);
}
if (expectedSize) {
// maybe we don't have a checksum, but at least CDN returns content length
// which we can check against the file size
debug$6('only checking expected file size %d', expectedSize);
const filesize = yield xvfb.util.getFileSize(filename);
if (filesize === expectedSize) {
debug$6('downloaded file has the expected size ✅');
return;
}
debug$6('raising error: file size mismatch');
const text = commonTags.stripIndent `
Corrupted download
Expected downloaded file to have size: ${expectedSize}
Computed size: ${filesize}
`;
throw new Error(text);
}
debug$6('downloaded file lacks checksum or size to verify');
return;
});
// downloads from given url
// return an object with
// {filename: ..., downloaded: true}
const downloadFromUrl = ({ url, downloadDestination, progress, ca, version, redirectTTL = defaultMaxRedirects }) => {
if (redirectTTL <= 0) {
return Promise.reject(new Error(commonTags.stripIndent `
Failed downloading the Cypress binary.
There were too many redirects. The default allowance is ${defaultMaxRedirects}.
Maybe you got stuck in a redirect loop?
`));
}
return new Bluebird((resolve, reject) => {
const proxy = getProxyForUrlWithNpmConfig(url);
debug$6('Downloading package', {
url,
proxy,
downloadDestination,
});
if (ca) {
debug$6('using custom CA details from npm config');
}
const reqOptions = Object.assign(Object.assign(Object.assign({ uri: url }, (proxy ? { proxy } : {})), (ca ? { agentOptions: { ca } } : {})), { method: 'GET', followRedirect: false });
const req = request(reqOptions);
// closure
let started = null;
let expectedSize;
let expectedChecksum;
requestProgress(req, {
throttle: progress.throttle,
})
.on('response', (response) => {
// we have computed checksum and filesize during test runner binary build
// and have set it on the S3 object as user meta data, available via
// these custom headers "x-amz-meta-..."
// see https://github.com/cypress-io/cypress/pull/4092
expectedSize = response.headers['x-amz-meta-size'] ||
response.headers['content-length'];
expectedChecksum = response.headers['x-amz-meta-checksum'];
if (expectedChecksum) {
debug$6('expected checksum %s', expectedChecksum);
}
if (expectedSize) {
// convert from string (all Amazon custom headers are strings)
expectedSize = Number(expectedSize);
debug$6('expected file size %d', expectedSize);
}
// start counting now once we've gotten
// response headers
started = new Date();
if (/^3/.test(response.statusCode)) {
const redirectVersion = response.headers['x-version'];
const redirectUrl = response.headers.location;
debug$6('redirect version:', redirectVersion);
debug$6('redirect url:', redirectUrl);
downloadFromUrl({ url: redirectUrl, progress, ca, downloadDestination, version: redirectVersion, redirectTTL: redirectTTL - 1 })
.then(resolve).catch(reject);
// if our status code does not start with 200
}
else if (!/^2/.test(response.statusCode)) {
debug$6('response code %d', response.statusCode);
const err = new Error(commonTags.stripIndent `
Failed downloading the Cypress binary.
Response code: ${response.statusCode}
Response message: ${response.statusMessage}
`);
reject(err);
// status codes here are all 2xx
}
else {
// We only enable this pipe connection when we know we've got a successful return
// and handle the completion with verify and resolve
// there was a possible race condition between end of request and close of writeStream
// that is made ordered with this Promise.all
Bluebird.all([new Bluebird((r) => {
return response.pipe(fs.createWriteStream(downloadDestination).on('close', r));
}), new Bluebird((r) => response.on('end', r))])
.then(() => {
debug$6('downloading finished');
verifyDownloadedFile(downloadDestination, expectedSize, expectedChecksum)
.then(() => debug$6('verified'))
.then(() => resolve(version))
.catch(reject);
});
}
})
.on('error', (e) => {
if (e.code === 'ECONNRESET')
return; // sometimes proxies give ECONNRESET but we don't care
reject(e);
})
.on('progress', (state) => {
// total time we've elapsed
// starting on our first progress notification
const elapsed = +new Date() - +started;
// request-progress sends a value between 0 and 1
const percentage = xvfb.util.convertPercentToPercentage(state.percent);
const eta = xvfb.util.calculateEta(percentage, elapsed);
// send up our percent and seconds remaining
progress.onProgress(percentage, xvfb.util.secsRemaining(eta));
});
});
};
/**
* Download Cypress.zip from external versionUrl to local file.
* @param [string] version Could be "3.3.0" or full URL
* @param [string] downloadDestination Local filename to save as
*/
const start$3 = (opts) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
let { version, downloadDestination, progress, redirectTTL } = opts;
if (!downloadDestination) {
assert.ok(_.isString(downloadDestination) && !_.isEmpty(downloadDestination), 'missing download dir');
}
if (!progress) {
progress = { onProgress: () => {
return {};
} };
}
const arch = yield xvfb.util.getRealArch();
const versionUrl = getUrl(arch, version);
progress.throttle = 100;
debug$6('needed Cypress version: %s', version);
debug$6('source url %s', versionUrl);
debug$6(`downloading cypress.zip to "${downloadDestination}"`);
try {
// ensure download dir exists
yield fs.ensureDir(path.dirname(downloadDestination));
const ca = yield getCA();
return downloadFromUrl(Object.assign({ url: versionUrl, downloadDestination, progress, ca, version }, (redirectTTL ? { redirectTTL } : {})));
}
catch (err) {
return prettyDownloadErr(err, versionUrl);
}
});
const downloadModule = {
start: start$3,
getUrl,
getProxyForUrlWithNpmConfig,
getCA,
};
const debug$5 = Debug('cypress:cli:unzip');
const unzipTools = {
extract,
};
// expose this function for simple testing
const unzip = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
debug$5('unzipping from %s', zipFilePath);
debug$5('into', installDir);
if (!zipFilePath) {
throw new Error('Missing zip filename');
}
const startTime = Date.now();
let yauzlDoneTime = 0;
yield fs.ensureDir(installDir);
yield new Promise((resolve, reject) => {
return yauzl.open(zipFilePath, (err, zipFile) => {
yauzlDoneTime = Date.now();
if (err) {
debug$5('error using yauzl %s', err.message);
return reject(err);
}
const total = zipFile.entryCount;
debug$5('zipFile entries count', total);
const started = new Date();
let percent = 0;
let count = 0;
const notify = (percent) => {
const elapsed = +new Date() - +started;
const eta = xvfb.util.calculateEta(percent, elapsed);
progress.onProgress(percent, xvfb.util.secsRemaining(eta));
};
const tick = () => {
count += 1;
percent = ((count / total) * 100);
const displayPercent = percent.toFixed(0);
return notify(Number(displayPercent));
};
const unzipWithNode = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
debug$5('unzipping with node.js (slow)');
const opts = {
dir: installDir,
onEntry: tick,
};
debug$5('calling Node extract tool %s %o', zipFilePath, opts);
try {
yield unzipTools.extract(zipFilePath, opts);
debug$5('node unzip finished');
return resolve();
}
catch (err) {
const error = err || new Error('Unknown error with Node extract tool');
debug$5('error %s', error.message);
return reject(error);
}
});
const unzipFallback = _.once(unzipWithNode);
const unzipWithUnzipTool = () => {
debug$5('unzipping via `unzip`');
const inflatingRe = /inflating:/;
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]);
sp.on('error', (err) => {
debug$5('unzip tool error: %s', err.message);
unzipFallback();
});
sp.on('close', (code) => {
debug$5('unzip tool close with code %d', code);
if (code === 0) {
percent = 100;
notify(percent);
return resolve();
}
debug$5('`unzip` failed %o', { code });
return unzipFallback();
});
sp.stdout.on('data', (data) => {
if (inflatingRe.test(data)) {
return tick();
}
});
sp.stderr.on('data', (data) => {
debug$5('`unzip` stderr %s', data);
});
};
// we attempt to first unzip with the native osx
// ditto because its less likely to have problems
// with corruption, symlinks, or icons causing failures
// and can handle resource forks
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
const unzipWithOsx = () => {
debug$5('unzipping via `ditto`');
const copyingFileRe = /^copying file/;
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]);
// f-it just unzip with node
sp.on('error', (err) => {
debug$5(err.message);
unzipFallback();
});
sp.on('close', (code) => {
if (code === 0) {
// make sure we get to 100% on the progress bar
// because reading in lines is not really accurate
percent = 100;
notify(percent);
return resolve();
}
debug$5('`ditto` failed %o', { code });
return unzipFallback();
});
return readline.createInterface({
input: sp.stderr,
})
.on('line', (line) => {
if (copyingFileRe.test(line)) {
return tick();
}
});
};
switch (os.platform()) {
case 'darwin':
return unzipWithOsx();
case 'linux':
return unzipWithUnzipTool();
case 'win32':
return unzipWithNode();
default:
return;
}
});
});
debug$5('unzip completed %o', {
yauzlMs: yauzlDoneTime - startTime,
unzipMs: Date.now() - yauzlDoneTime,
});
});
function isMaybeWindowsMaxPathLengthError(err) {
return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath';
}
const start$2 = (_a) => xvfb.__awaiter(void 0, [_a], void 0, function* ({ zipFilePath, installDir, progress }) {
assert.ok(_.isString(installDir) && !_.isEmpty(installDir), 'missing installDir');
if (!progress) {
progress = { onProgress: () => {
return {};
} };
}
try {
const installDirExists = yield fs.pathExists(installDir);
if (installDirExists) {
debug$5('removing existing unzipped binary', installDir);
yield fs.remove(installDir);
}
yield unzip({ zipFilePath, installDir, progress });
}
catch (err) {
const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ?
xvfb.errors.failedUnzipWindowsMaxPathLength
: xvfb.errors.failedUnzip;
yield xvfb.throwFormErrorText(errorTemplate)(err);
}
});
const unzipModule = {
start: start$2,
utils: {
unzip,
unzipTools,
},
};
const debug$4 = Debug('cypress:cli');
function _getBinaryUrlFromBuildInfo(version, arch, { commitSha, commitBranch }) {
const platform = os.platform();
if ((platform === 'win32') && (arch === 'arm64')) {
debug$4(`detected platform ${platform} architecture ${arch} combination`);
arch = 'x64';
debug$4(`overriding to download ${platform}-${arch} pre-release binary instead`);
}
return `https://cdn.cypress.io/beta/binary/${version}/${platform}-${arch}/${commitBranch}-${commitSha}/cypress.zip`;
}
const alreadyInstalledMsg = () => {
if (!xvfb.util.isPostInstall()) {
xvfb.loggerModule.log(commonTags.stripIndent `
Skipping installation:
Pass the ${chalk.yellow('--force')} option if you'd like to reinstall anyway.
`);
}
};
const displayCompletionMsg = () => {
// check here to see if we are globally installed
if (xvfb.util.isInstalledGlobally()) {
// if we are display a warning
xvfb.loggerModule.log();
xvfb.loggerModule.warn(commonTags.stripIndent `
${logSymbols.warning} Warning: It looks like you\'ve installed Cypress globally.
The recommended way to install Cypress is as a devDependency per project.
You should probably run these commands:
- ${chalk.cyan('npm uninstall -g cypress')}
- ${chalk.cyan('npm install --save-dev cypress')}
`);
return;
}
xvfb.loggerModule.log();
xvfb.loggerModule.log('You can now open Cypress by running one of the following, depending on your package manager:');
xvfb.loggerModule.log();
xvfb.loggerModule.log(chalk.cyan('- npx cypress open'));
xvfb.loggerModule.log(chalk.cyan('- yarn cypress open'));
xvfb.loggerModule.log(chalk.cyan('- pnpm cypress open'));
xvfb.loggerModule.log();
xvfb.loggerModule.log(chalk.grey('https://on.cypress.io/opening-the-app'));
xvfb.loggerModule.log();
};
const downloadAndUnzip = ({ version, installDir, downloadDir }) => {
const progress = {
throttle: 100,
onProgress: null,
};
const downloadDestination = path.join(downloadDir, `cypress-${process.pid}.zip`);
const rendererOptions = getRendererOptions();
// let the user know what version of cypress we're downloading!
xvfb.loggerModule.log(`Installing Cypress ${chalk.gray(`(version: ${version})`)}`);
xvfb.loggerModule.log();
const tasks = new listr2.Listr([
{
options: { title: xvfb.util.titleize('Downloading Cypress') },
task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
// as our download progresses indicate the status
progress.onProgress = progessify(task, 'Downloading Cypress');
const redirectVersion = yield downloadModule.start({ version, downloadDestination, progress });
if (redirectVersion)
version = redirectVersion;
debug$4(`finished downloading file: ${downloadDestination}`);
// save the download destination for unzipping
xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Downloaded Cypress')), rendererOptions.renderer);
}),
},
unzipTask({
progress,
zipFilePath: downloadDestination,
installDir,
rendererOptions,
}),
{
options: { title: xvfb.util.titleize('Finishing Installation') },
task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const cleanup = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
debug$4('removing zip file %s', downloadDestination);
yield fs.remove(downloadDestination);
});
yield cleanup();
debug$4('finished installation in', installDir);
xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir)), rendererOptions.renderer);
}),
},
], { rendererOptions });
// start the tasks!
return tasks.run();
};
const validateOS = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const platformInfo = yield xvfb.util.getPlatformInfo();
return platformInfo.match(/(win32-x64|win32-arm64|linux-x64|linux-arm64|darwin-x64|darwin-arm64)/);
});
/**
* Returns the version to install - either a string like `1.2.3` to be fetched
* from the download server or a file path or HTTP URL.
*/
function getVersionOverride(version, { arch, envVarVersion, buildInfo }) {
// let this environment variable reset the binary version we need
if (envVarVersion) {
return envVarVersion;
}
if (buildInfo && !buildInfo.stable) {
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
${logSymbols.warning} Warning: You are installing a pre-release build of Cypress.
Bugs may be present which do not exist in production builds.
This build was created from:
* Commit SHA: ${buildInfo.commitSha}
* Commit Branch: ${buildInfo.commitBranch}
* Commit Timestamp: ${buildInfo.commitDate}
`));
xvfb.loggerModule.log();
return _getBinaryUrlFromBuildInfo(version, arch, buildInfo);
}
}
function getEnvVarVersion() {
if (!xvfb.util.getEnv('CYPRESS_INSTALL_BINARY'))
return;
// because passed file paths are often double quoted
// and might have extra whitespace around, be robust and trim the string
const trimAndRemoveDoubleQuotes = true;
const envVarVersion = xvfb.util.getEnv('CYPRESS_INSTALL_BINARY', trimAndRemoveDoubleQuotes);
debug$4('using environment variable CYPRESS_INSTALL_BINARY "%s"', envVarVersion);
return envVarVersion;
}
const start$1 = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
debug$4('installing with options %j', options);
const envVarVersion = getEnvVarVersion();
if (envVarVersion === '0') {
debug$4('environment variable CYPRESS_INSTALL_BINARY = 0, skipping install');
xvfb.loggerModule.log(commonTags.stripIndent `
${chalk.yellow('Note:')} Skipping binary installation: Environment variable CYPRESS_INSTALL_BINARY = 0.`);
xvfb.loggerModule.log();
return;
}
const pkgPath = xvfb.relativeToRepoRoot('package.json');
if (!pkgPath) {
return xvfb.throwFormErrorText('Could not find package.json for Cypress package to determine build information')();
}
const { buildInfo, version } = JSON.parse(yield promises.readFile(pkgPath, 'utf8'));
_.defaults(options, {
force: false,
buildInfo,
});
if (xvfb.util.getEnv('CYPRESS_CACHE_FOLDER')) {
const envCache = xvfb.util.getEnv('CYPRESS_CACHE_FOLDER');
xvfb.loggerModule.log(commonTags.stripIndent `
${chalk.yellow('Note:')} Overriding Cypress cache directory to: ${chalk.cyan(envCache)}
Previous installs of Cypress may not be found.
`);
xvfb.loggerModule.log();
}
const pkgVersion = xvfb.util.pkgVersion();
const arch = yield xvfb.util.getRealArch();
const versionOverride = getVersionOverride(version, { arch, envVarVersion, buildInfo: options.buildInfo });
const versionToInstall = versionOverride || pkgVersion;
debug$4('version in package.json is %s, version to install is %s', pkgVersion, versionToInstall);
const installDir = xvfb.stateModule.getVersionDir(pkgVersion, options.buildInfo);
const cacheDir = xvfb.stateModule.getCacheDir();
const binaryDir = xvfb.stateModule.getBinaryDir(pkgVersion);
if (!(yield validateOS())) {
return xvfb.throwFormErrorText(xvfb.errors.invalidOS)();
}
try {
yield fs.ensureDir(cacheDir);
}
catch (err) {
if (err.code === 'EACCES') {
return xvfb.throwFormErrorText(xvfb.errors.invalidCacheDirectory)(commonTags.stripIndent `
Failed to access ${chalk.cyan(cacheDir)}:
${err.message}
`);
}
throw err;
}
const binaryPkg = yield xvfb.stateModule.getBinaryPkgAsync(binaryDir);
const binaryVersion = yield xvfb.stateModule.getBinaryPkgVersion(binaryPkg);
const shouldInstall = () => {
if (!binaryVersion) {
debug$4('no binary installed under cli version');
return true;
}
xvfb.loggerModule.log();
xvfb.loggerModule.log(commonTags.stripIndent `
Cypress ${chalk.green(binaryVersion)} is installed in ${chalk.cyan(installDir)}
`);
xvfb.loggerModule.log();
if (options.force) {
debug$4('performing force install over existing binary');
return true;
}
if ((binaryVersion === versionToInstall) || !xvfb.util.isSemver(versionToInstall)) {
// our version matches, tell the user this is a noop
alreadyInstalledMsg();
return false;
}
return true;
};
// noop if we've been told not to download
if (!shouldInstall()) {
return debug$4('Not downloading or installing binary');
}
if (envVarVersion) {
xvfb.loggerModule.log(chalk.yellow(commonTags.stripIndent `
${logSymbols.warning} Warning: Forcing a binary version different than the default.
The CLI expected to install version: ${chalk.green(pkgVersion)}
Instead we will install version: ${chalk.green(versionToInstall)}
These versions may not work properly together.
`));
xvfb.loggerModule.log();
}
const getLocalFilePath = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
// see if version supplied is a path to a binary
if (yield fs.pathExists(versionToInstall)) {
return path.extname(versionToInstall) === '.zip' ? versionToInstall : false;
}
const possibleFile = xvfb.util.formAbsolutePath(versionToInstall);
debug$4('checking local file', possibleFile, 'cwd', process.cwd());
// if this exists return the path to it
// else false
if ((yield fs.pathExists(possibleFile)) && path.extname(possibleFile) === '.zip') {
return possibleFile;
}
return false;
});
const pathToLocalFile = yield getLocalFilePath();
if (pathToLocalFile) {
const absolutePath = path.resolve(versionToInstall);
debug$4('found local file at', absolutePath);
debug$4('skipping download');
const rendererOptions = getRendererOptions();
return new listr2.Listr([unzipTask({
progress: {
throttle: 100,
onProgress: null,
},
zipFilePath: absolutePath,
installDir,
rendererOptions,
})], { rendererOptions }).run();
}
if (options.force) {
debug$4('Cypress already installed at', installDir);
debug$4('but the installation was forced');
}
debug$4('preparing to download and unzip version ', versionToInstall, 'to path', installDir);
const downloadDir = os.tmpdir();
yield downloadAndUnzip({ version: versionToInstall, installDir, downloadDir });
// delay 1 sec for UX, unless we are testing
yield timers.setTimeout(1000);
displayCompletionMsg();
});
const unzipTask = ({ zipFilePath, installDir, progress, rendererOptions }) => {
return {
options: { title: xvfb.util.titleize('Unzipping Cypress') },
task: (ctx, task) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
// as our unzip progresses indicate the status
progress.onProgress = progessify(task, 'Unzipping Cypress');
yield unzipModule.start({ zipFilePath, installDir, progress });
xvfb.util.setTaskTitle(task, xvfb.util.titleize(chalk.green('Unzipped Cypress')), rendererOptions.renderer);
}),
};
};
const progessify = (task, title) => {
// return higher order function
return (percentComplete, remaining) => {
const percentCompleteStr = chalk.white(` ${percentComplete}%`);
// pluralize seconds remaining
const remainingStr = chalk.gray(`${remaining}s`);
xvfb.util.setTaskTitle(task, xvfb.util.titleize(title, percentCompleteStr, remainingStr), getRendererOptions().renderer);
};
};
// if we are running in CI then use
// the verbose renderer else use
// the default
const getRendererOptions = () => {
let renderer = xvfb.util.isCi() ? spawn.VerboseRenderer : 'default';
if (xvfb.loggerModule.logLevel() === 'silent') {
renderer = 'silent';
}
return {
renderer,
};
};
var installModule = {
start: start$1,
_getBinaryUrlFromBuildInfo,
};
/**
* Throws an error with "details" property from
* "errors" object.
* @param {Object} details - Error details
*/
const throwInvalidOptionError = (details) => {
if (!details) {
details = xvfb.errors.unknownError;
}
// throw this error synchronously, it will be caught later on and
// the details will be propagated to the promise chain
const err = new Error();
err.details = details;
throw err;
};
/**
* Selects exec args based on the configured `testingType`
* @param {string} testingType The type of tests being executed
* @returns {string[]} The array of new exec arguments
*/
const processTestingType = (options) => {
if (options.e2e && options.component) {
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
}
if (options.testingType && (options.component || options.e2e)) {
return throwInvalidOptionError(xvfb.errors.incompatibleTestTypeFlags);
}
if (options.testingType === 'component' || options.component || options.ct) {
return ['--testing-type', 'component'];
}
if (options.testingType === 'e2e' || options.e2e) {
return ['--testing-type', 'e2e'];
}
if (options.testingType) {
return throwInvalidOptionError(xvfb.errors.invalidTestingType);
}
return [];
};
/**
* Throws an error if configFile is string 'false' or boolean false
* @param {*} options
*/
const checkConfigFile = (options) => {
// CLI will parse as string, module API can pass in boolean
if (options.configFile === 'false' || options.configFile === false) {
throwInvalidOptionError(xvfb.errors.invalidConfigFile);
}
};
const debug$3 = Debug('cypress:cli');
/**
* Maps options collected by the CLI
* and forms list of CLI arguments to the server.
*
* Note: there is lightweight validation, with errors
* thrown synchronously.
*
* @returns {string[]} list of CLI arguments
*/
const processOpenOptions = (options = {}) => {
// In addition to setting the project directory, setting the project option
// here ultimately decides whether cypress is run in global mode or not.
// It's first based off whether it's installed globally by npm/yarn (-g).
// A global install can be overridden by the --project flag, putting Cypress
// in project mode. A non-global install can be overridden by the --global
// flag, putting it in global mode.
if (!xvfb.util.isInstalledGlobally() && !options.global && !options.project) {
options.project = process.cwd();
}
const args = [];
if (options.config) {
args.push('--config', options.config);
}
if (options.configFile !== undefined) {
checkConfigFile(options);
args.push('--config-file', options.configFile);
}
if (options.browser) {
args.push('--browser', options.browser);
}
if (options.env) {
args.push('--env', options.env);
}
if (options.expose) {
args.push('--expose', options.expose);
}
if (options.port) {
args.push('--port', options.port);
}
if (options.project) {
args.push('--project', options.project);
}
if (options.global) {
args.push('--global', options.global);
}
if (options.inspect) {
args.push('--inspect');
}
if (options.inspectBrk) {
args.push('--inspectBrk');
}
args.push(...processTestingType(options));
debug$3('opening from options %j', options);
debug$3('command line arguments %j', args);
return args;
};
const start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
function open() {
try {
const args = processOpenOptions(options);
return spawn.start$1(args, {
dev: options.dev,
detached: Boolean(options.detached),
});
}
catch (err) {
if (err.details) {
return xvfb.exitWithError(err.details)();
}
throw err;
}
}
if (options.dev) {
return open();
}
yield spawn.start();
return open();
});
var openModule = {
start,
processOpenOptions,
};
const debug$2 = Debug('cypress:cli:run');
/**
* Typically a user passes a string path to the project.
* But "cypress open" allows using `false` to open in global mode,
* and the user can accidentally execute `cypress run --project false`
* which should be invalid.
*/
const isValidProject = (v) => {
if (typeof v === 'boolean') {
return false;
}
if (v === '' || v === 'false' || v === 'true') {
return false;
}
return true;
};
/**
* Maps options collected by the CLI
* and forms list of CLI arguments to the server.
*
* Note: there is lightweight validation, with errors
* thrown synchronously.
*
* @returns {string[]} list of CLI arguments
*/
const processRunOptions = (options = {}) => {
debug$2('processing run options %o', options);
if (!isValidProject(options.project)) {
debug$2('invalid project option %o', { project: options.project });
return throwInvalidOptionError(xvfb.errors.invalidRunProjectPath);
}
const args = ['--run-project', options.project];
if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) {
args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures);
}
if (options.browser) {
args.push('--browser', options.browser);
}
if (options.ciBuildId) {
args.push('--ci-build-id', options.ciBuildId);
}
if (options.config) {
args.push('--config', options.config);
}
if (options.configFile !== undefined) {
checkConfigFile(options);
args.push('--config-file', options.configFile);
}
if (options.env) {
args.push('--env', options.env);
}
if (options.expose) {
args.push('--expose', options.expose);
}
if (options.exit === false) {
args.push('--no-exit');
}
if (options.group) {
args.push('--group', options.group);
}
if (options.headed) {
args.push('--headed', options.headed);
}
if (options.headless) {
if (options.headed) {
return throwInvalidOptionError(xvfb.errors.incompatibleHeadlessFlags);
}
args.push('--headed', String(!options.headless));
}
// if key is set use that - else attempt to find it by environment variable
if (options.key == null) {
debug$2('--key is not set, looking up environment variable CYPRESS_RECORD_KEY');
options.key = xvfb.util.getEnv('CYPRESS_RECORD_KEY');
}
// if we have a key assume we're in record mode
if (options.key) {
args.push('--key', options.key);
}
if (options.outputPath) {
args.push('--output-path', options.outputPath);
}
if (options.parallel) {
args.push('--parallel');
}
if (options.passWithNoTests) {
args.push('--pass-with-no-tests');
}
if (options.posixExitCodes) {
args.push('--posix-exit-codes');
}
if (options.port) {
args.push('--port', options.port);
}
if (options.quiet) {
args.push('--quiet');
}
// if record is defined and we're not
// already in ci mode, then send it up
if (options.record != null) {
args.push('--record', options.record);
}
// if we have a specific reporter push that into the args
if (options.reporter) {
args.push('--reporter', options.reporter);
}
// if we have a specific reporter push that into the args
if (options.reporterOptions) {
args.push('--reporter-options', options.reporterOptions);
}
if (options.runnerUi != null) {
args.push('--runner-ui', options.runnerUi);
}
// if we have specific spec(s) push that into the args
if (options.spec) {
args.push('--spec', options.spec);
}
if (options.tag) {
args.push('--tag', options.tag);
}
if (options.inspect) {
args.push('--inspect');
}
if (options.inspectBrk) {
args.push('--inspectBrk');
}
args.push(...processTestingType(options));
return args;
};
const runModule = {
processRunOptions,
isValidProject,
// resolves with the number of failed tests
start() {
return xvfb.__awaiter(this, arguments, void 0, function* (options = {}) {
_.defaults(options, {
key: null,
spec: null,
reporter: null,
reporterOptions: null,
project: process.cwd(),
});
function run() {
try {
const args = processRunOptions(options);
debug$2('run to spawn.start args %j', args);
return spawn.start$1(args, {
dev: options.dev,
});
}
catch (err) {
if (err.details) {
return xvfb.exitWithError(err.details)();
}
throw err;
}
}
if (options.dev) {
return run();
}
yield spawn.start();
return run();
});
},
};
/**
* Get the size of a folder or a file.
*
* This function returns the actual file size of the folder (size), not the allocated space on disk (size on disk).
* For more details between the difference, check this link:
* https://www.howtogeek.com/180369/why-is-there-a-big-difference-between-size-and-size-on-disk/
*
* @param {string} path path to the file or the folder.
*/
function getSize(path$1) {
return xvfb.__awaiter(this, void 0, void 0, function* () {
const stat = yield fs.lstat(path$1);
if (stat.isDirectory()) {
const list = yield fs.readdir(path$1);
return Bluebird.resolve(list).reduce((prev, curr) => xvfb.__awaiter(this, void 0, void 0, function* () {
const currPath = path.join(path$1, curr);
const s = yield fs.lstat(currPath);
if (s.isDirectory()) {
return prev + (yield getSize(currPath));
}
return prev + s.size;
}), 0);
}
return stat.size;
});
}
dayjs.extend(relativeTime);
// output colors for the table
const colors = {
titles: chalk.white,
dates: chalk.cyan,
values: chalk.green,
size: chalk.gray,
};
const logCachePath = () => {
xvfb.loggerModule.always(xvfb.stateModule.getCacheDir());
return undefined;
};
const clear = () => {
return fs.remove(xvfb.stateModule.getCacheDir());
};
const prune = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const cacheDir = xvfb.stateModule.getCacheDir();
const checkedInBinaryVersion = xvfb.util.pkgVersion();
let deletedBinary = false;
try {
const versions = yield fs.readdir(cacheDir);
for (const version of versions) {
if (version !== checkedInBinaryVersion) {
deletedBinary = true;
const versionDir = path.join(cacheDir, version);
yield fs.remove(versionDir);
}
}
if (deletedBinary) {
xvfb.loggerModule.always(`Deleted all binary caches except for the ${checkedInBinaryVersion} binary cache.`);
}
else {
xvfb.loggerModule.always(`No binary caches found to prune.`);
}
}
catch (e) {
if (e.code === 'ENOENT') {
xvfb.loggerModule.always(`No Cypress cache was found at ${cacheDir}. Nothing to prune.`);
return;
}
throw e;
}
});
const fileSizeInMB = (size) => {
return `${(size / 1024 / 1024).toFixed(1)}MB`;
};
/**
* Collects all cached versions, finds when each was used
* and prints a table with results to the terminal
*/
const list = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (showSize = false) {
const binaries = yield getCachedVersions(showSize);
const head = [colors.titles('version'), colors.titles('last used')];
if (showSize) {
head.push(colors.titles('size'));
}
const table = new Table({
head,
});
binaries.forEach((binary) => {
const versionString = colors.values(binary.version);
const lastUsed = binary.accessed ? colors.dates(binary.accessed) : 'unknown';
const row = [versionString, lastUsed];
if (showSize) {
const size = colors.size(fileSizeInMB(binary.size));
row.push(size);
}
return table.push(row);
});
xvfb.loggerModule.always(table.toString());
});
const getCachedVersions = (showSize) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const cacheDir = xvfb.stateModule.getCacheDir();
const versions = yield fs.readdir(cacheDir);
const filteredVersions = versions.filter(xvfb.util.isSemver).map((version) => {
return {
version,
folderPath: path.join(cacheDir, version),
};
});
const binaries = [];
for (const binary of filteredVersions) {
const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
const executable = xvfb.stateModule.getPathToExecutable(binaryDir);
try {
const stat = yield fs.stat(executable);
const lastAccessedTime = _.get(stat, 'atime');
if (lastAccessedTime) {
const accessed = dayjs(lastAccessedTime).fromNow();
// @ts-expect-error - accessed is not defined in the type
binary.accessed = accessed;
}
// if no lastAccessedTime
// the test runner has never been opened
// or could be a test simulating missing timestamp
}
catch (e) {
// could not find the binary or gets its stats
// no-op
}
if (showSize) {
const binaryDir = xvfb.stateModule.getBinaryDir(binary.version);
const size = yield getSize(binaryDir);
binaries.push(Object.assign(Object.assign({}, binary), { size }));
}
else {
binaries.push(binary);
}
}
return binaries;
});
const cacheModule = {
path: logCachePath,
clear,
prune,
list,
getCachedVersions,
};
const debug$1 = Debug('cypress:cli');
const getBinaryDirectory = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
if (xvfb.util.getEnv('CYPRESS_RUN_BINARY')) {
let envBinaryPath = path.resolve(xvfb.util.getEnv('CYPRESS_RUN_BINARY'));
try {
const envBinaryDir = yield xvfb.stateModule.parseRealPlatformBinaryFolderAsync(envBinaryPath);
if (!envBinaryDir) {
const raiseErrorFn = xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath));
yield raiseErrorFn();
}
debug$1('CYPRESS_RUN_BINARY has binaryDir:', envBinaryDir);
return envBinaryDir;
}
catch (err) {
const raiseErrorFn = xvfb.throwFormErrorText(xvfb.errors.CYPRESS_RUN_BINARY.notValid(envBinaryPath));
yield raiseErrorFn(err.message);
}
}
return xvfb.stateModule.getBinaryDir();
});
const getVersions = () => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const binDir = yield getBinaryDirectory();
const pkg = yield xvfb.stateModule.getBinaryPkgAsync(binDir);
const versions = {
binary: xvfb.stateModule.getBinaryPkgVersion(pkg),
electronVersion: xvfb.stateModule.getBinaryElectronVersion(pkg),
electronNodeVersion: xvfb.stateModule.getBinaryElectronNodeVersion(pkg),
};
debug$1('binary versions %o', versions);
const buildInfo = xvfb.util.pkgBuildInfo();
let packageVersion = xvfb.util.pkgVersion();
if (!buildInfo)
packageVersion += ' (development)';
else if (!buildInfo.stable)
packageVersion += ' (pre-release)';
const versionsFinal = {
package: packageVersion,
binary: versions.binary || 'not installed',
electronVersion: versions.electronVersion || 'not found',
electronNodeVersion: versions.electronNodeVersion || 'not found',
};
debug$1('combined versions %o', versions);
return versionsFinal;
});
const versionsModule = {
getVersions,
};
// color for numbers and show values
const g = chalk.green;
// color for paths
const p = chalk.cyan;
const red = chalk.red;
// urls
const link = chalk.blue.underline;
// to be exported
const methods = {};
methods.findProxyEnvironmentVariables = () => {
return _.pick(process.env, ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY']);
};
const maskSensitiveVariables = (obj) => {
const masked = Object.assign({}, obj);
if (masked.CYPRESS_RECORD_KEY) {
masked.CYPRESS_RECORD_KEY = '<redacted>';
}
return masked;
};
methods.findCypressEnvironmentVariables = () => {
const isCyVariable = (val, key) => key.startsWith('CYPRESS_');
return _.pickBy(process.env, isCyVariable);
};
const formatCypressVariables = () => {
const vars = methods.findCypressEnvironmentVariables();
return maskSensitiveVariables(vars);
};
methods.start = (...args_1) => xvfb.__awaiter(void 0, [...args_1], void 0, function* (options = {}) {
const args = ['--mode=info'];
yield spawn.start$1(args, {
dev: options.dev,
});
console.log();
const proxyVars = methods.findProxyEnvironmentVariables();
if (_.isEmpty(proxyVars)) {
console.log('Proxy Settings: none detected');
}
else {
console.log('Proxy Settings:');
_.forEach(proxyVars, (value, key) => {
console.log('%s: %s', key, g(value));
});
console.log();
console.log('Learn More: %s', link('https://on.cypress.io/proxy-configuration'));
console.log();
}
const cyV