cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
1,269 lines (1,244 loc) • 81.5 kB
JavaScript
'use strict';
var xvfb = require('./xvfb-SgoMy1_9.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-BZQfzolX.js');
var os = require('os');
var listr2 = require('listr2');
var timers = require('timers/promises');
var fsp = 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 fs$1 = require('fs');
var util = require('util');
var require$$0 = require('stream');
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 pipelineAsync = util.promisify(require$$0.pipeline);
// Unix file mode masks for entries stored in zip's externalFileAttributes
// (the high 16 bits when the file was zipped on a Unix host).
const S_IFMT = 0o170000;
const S_IFDIR = 0o040000;
const S_IFLNK = 0o120000;
// PATH_MAX on Linux/macOS is 4096; symlink targets larger than this are not
// legal filesystem paths and almost certainly indicate a malformed or
// malicious archive. The cap also prevents reading an arbitrarily large
// "symlink" entry into memory.
const MAX_SYMLINK_TARGET_BYTES = 4096;
/**
* Extracts the contents of a zip archive into the given destination directory.
* Recreates directories, regular files, and symlinks while preserving Unix
* file modes encoded in each entry's external attributes. Calls `onEntry` once
* per archive entry processed. Refuses entries whose resolved path would
* escape the destination directory.
*/
const extractWithYauzl = (zipFilePath, destDir, onEntry) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const resolvedDest = path.resolve(destDir);
yield new Promise((resolve, reject) => {
// autoClose: false — `finish` below owns closing the zipfile, so we don't
// want yauzl's internal end-listener closing it first and tripping a
// double-close (EBADF) when we do.
yauzl.open(zipFilePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
if (err) {
return reject(err);
}
// `settled` guards against an in-flight `handleEntry` calling
// `zipFile.readEntry()` on a now-closed handle when extraction has
// already failed (e.g. yauzl emitted 'error' while we were writing
// an entry to disk).
let settled = false;
const finish = _.once((err) => {
var _a, _b;
settled = true;
(_a = zipFile.removeAllListeners) === null || _a === void 0 ? void 0 : _a.call(zipFile);
(_b = zipFile.close) === null || _b === void 0 ? void 0 : _b.call(zipFile);
if (err) {
return reject(err);
}
return resolve();
});
// Normalize any thrown / emitted value into a real Error so that a
// falsy rejection (e.g. `Promise.reject(undefined)`) doesn't get
// misread by `finish` as a successful completion.
const fail = (err) => {
finish(err instanceof Error ? err : new Error(typeof err === 'string' && err ? err : 'zip extraction failed'));
};
zipFile.on('error', fail);
zipFile.on('end', () => finish());
zipFile.on('entry', (entry) => {
handleEntry(zipFile, entry, resolvedDest)
.then(() => {
if (settled)
return;
onEntry();
zipFile.readEntry();
})
.catch(fail);
});
zipFile.readEntry();
});
});
});
const handleEntry = (zipFile, entry, resolvedDest) => xvfb.__awaiter(void 0, void 0, void 0, function* () {
const fileDest = path.resolve(resolvedDest, entry.fileName);
// refuse anything that would write outside the install dir
if (fileDest !== resolvedDest &&
!fileDest.startsWith(resolvedDest + path.sep)) {
throw new Error(`Refusing to extract entry outside of destination: ${entry.fileName}`);
}
const unixMode = (entry.externalFileAttributes >>> 16) & 0xffff;
// Some archivers mark directories by Unix mode bits instead of (or in
// addition to) a trailing slash; honor both so we don't extract a
// directory entry as a zero-byte file.
const isDir = /\/$/.test(entry.fileName) || (unixMode & S_IFMT) === S_IFDIR;
const isSymlink = (unixMode & S_IFMT) === S_IFLNK;
if (isDir) {
yield fsp.mkdir(fileDest, { recursive: true });
return;
}
yield fsp.mkdir(path.dirname(fileDest), { recursive: true });
if (isSymlink) {
if (entry.uncompressedSize > MAX_SYMLINK_TARGET_BYTES) {
throw new Error(`Refusing to extract symlink with target larger than ${MAX_SYMLINK_TARGET_BYTES} bytes: ${entry.fileName}`);
}
const linkTarget = yield readEntryAsString(zipFile, entry, MAX_SYMLINK_TARGET_BYTES);
const resolvedTarget = path.resolve(path.dirname(fileDest), linkTarget);
if (resolvedTarget !== resolvedDest &&
!resolvedTarget.startsWith(resolvedDest + path.sep)) {
throw new Error(`Refusing to extract symlink pointing outside of destination: ${entry.fileName} -> ${linkTarget}`);
}
yield fsp.rm(fileDest, { recursive: true, force: true });
yield fsp.symlink(linkTarget, fileDest);
return;
}
const readStream = yield new Promise((res, rej) => {
zipFile.openReadStream(entry, (err, rs) => {
if (err) {
return rej(err);
}
return res(rs);
});
});
// Preserve the Unix mode bits when present; otherwise fall back to a sane default.
const fileMode = (unixMode & 0o7777) || 0o644;
const writeStream = fs$1.createWriteStream(fileDest, { mode: fileMode });
yield pipelineAsync(readStream, writeStream);
});
const readEntryAsString = (zipFile, entry, maxBytes) => {
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err, rs) => {
if (err) {
return reject(err);
}
const chunks = [];
let received = 0;
let bailed = false;
const bail = (err) => {
var _a;
if (bailed)
return;
bailed = true;
(_a = rs.destroy) === null || _a === void 0 ? void 0 : _a.call(rs, err);
reject(err);
};
rs.on('data', (chunk) => {
received += chunk.length;
if (received > maxBytes) {
bail(new Error(`Refusing to read entry body larger than ${maxBytes} bytes: ${entry.fileName}`));
return;
}
chunks.push(chunk);
});
rs.on('end', () => {
if (bailed)
return;
resolve(Buffer.concat(chunks).toString('utf8'));
});
rs.on('error', bail);
});
});
};
const debug$5 = Debug('cypress:cli:unzip');
const unzipTools = {
extractWithYauzl,
};
// 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) => {
// Open with lazyEntries so yauzl doesn't auto-emit entries (which would
// require the fd to stay open for the duration of the OS-tool extraction).
// We only need the entryCount here for the progress calculation.
return yauzl.open(zipFilePath, { lazyEntries: true }, (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);
// Close the count-only handle — the Node fallback re-opens the zip for extraction.
zipFile.close();
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)');
try {
yield unzipTools.extractWithYauzl(zipFilePath, installDir, tick);
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:install');
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 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 fsp.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 instanceof Error && 'code' in err && err.code === 'EACCES') {
return xvfb.throwFormErrorText(xvfb.errors.invalidCacheDirectory)(commonTags.stripIndent `
Failed to access ${chalk.cyan(cacheDir)}:
${err.message}
`);
}
else {
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();
const tasks = pathToLocalFile ?
installFromLocal(pathToLocalFile, installDir) :
installFromRemote(versionToInstall, installDir);
if (options.force) {
debug$4('Cypress already installed at', installDir);
debug$4('but the installation was forced');
}
// let the user know what version of cypress we're downloading!
xvfb.loggerModule.log(`Installing Cypress ${chalk.gray(`(version: ${versionToInstall})`)}`);
xvfb.loggerModule.log();
const taskRunner = new listr2.Listr(tasks, Object.assign(Object.assign({
// In CI we want timestamped, line-per-event output. Locally,
// the default in-place spinner is the better experience.
renderer: xvfb.util.isCi() ? 'verbose' : 'default' }, (xvfb.util.isCi() && { rendererOptions: { timestamp: listr2.PRESET_TIMESTAMP } })), { silentRendererCondition: () => xvfb.loggerModule.logLevel() === 'silent' }));
yield taskRunner.run();
// delay 1 sec for UX, unless we are testing
yield timers.setTimeout(1000);
displayCompletionMsg();
});
function downloadArchive(version, downloadDestination) {
const inProgressTitle = 'Downloading Cypress';
const completedTitle = chalk.green('Downloaded Cypress');
return {
title: xvfb.util.titleize(inProgressTitle),
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
yield downloadModule.start({
version,
downloadDestination,
progress: {
throttle: 100,
onProgress: (percentComplete, remaining) => {
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
},
},
});
debug$4(`finished downloading file: ${downloadDestination}`);
task.title = xvfb.util.titleize(completedTitle);
}),
};
}
function installFromLocal(pathToLocalFile, installDir) {
const zipFilePath = path.resolve(pathToLocalFile);
debug$4('found local file at', zipFilePath);
debug$4('skipping download');
return [
unzipArchive(zipFilePath, installDir),
];
}
function installFromRemote(version, installDir) {
const downloadDestination = path.join(os.tmpdir(), `cypress-${process.pid}.zip`);
debug$4('preparing to download and unzip version ', version, 'to path', installDir);
return [
downloadArchive(version, downloadDestination),
unzipArchive(downloadDestination, installDir),
cleanup(downloadDestination, installDir),
];
}
function unzipArchive(zipFilePath, installDir) {
const inProgressTitle = 'Unzipping Cypress';
const completedTitle = chalk.green('Unzipped Cypress');
return {
title: xvfb.util.titleize(inProgressTitle),
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
yield unzipModule.start({
zipFilePath,
installDir,
progress: {
onProgress: (percentComplete, remaining) => {
task.title = progressTitle(inProgressTitle, percentComplete, remaining);
},
},
});
task.title = xvfb.util.titleize(completedTitle);
}),
};
}
function cleanup(archiveLocation, installDir) {
return {
title: xvfb.util.titleize('Finishing Installation'),
task: (ctx, task) => xvfb.__awaiter(this, void 0, void 0, function* () {
debug$4('removing zip file %s', archiveLocation);
yield fs.remove(archiveLocation);
debug$4('finished installation in', installDir);
task.title = xvfb.util.titleize(chalk.green('Finished Installation'), chalk.gray(installDir));
}),
};
}
function progressTitle(title, percentComplete, remaining) {
return xvfb.util.titleize(title, chalk.white(` ${percentComplete}%`), chalk.gray(`${remaining}s`));
}
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);
// Subdirs under the cache root that are not binary version dirs.
const EXTERNAL_CACHE_ENTRIES = new Set(['bundles']);
// 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 (EXTERNAL_CACHE_ENTRIES.has(version))
continue;
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 prun