cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
1,168 lines (1,109 loc) • 41.9 kB
JavaScript
;
var os = require('os');
var Bluebird = require('bluebird');
var Xvfb = require('@cypress/xvfb');
var commonTags = require('common-tags');
var Debug = require('debug');
var chalk = require('chalk');
var _ = require('lodash');
var assert = require('assert');
var arch = require('arch');
var ospath = require('ospath');
var hasha = require('hasha');
var tty = require('tty');
var path = require('path');
var ciInfo = require('ci-info');
var execa = require('execa');
var si = require('systeminformation');
var cachedir = require('cachedir');
var logSymbols = require('log-symbols');
var executable = require('executable');
var process$1 = require('process');
var supportsColor = require('supports-color');
var isInstalledGlobally = require('is-installed-globally');
var fs$1 = require('fs-extra');
var fs = require('fs');
var untildify = require('untildify');
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
let logs = [];
const logLevel = () => {
return (process.env.npm_config_loglevel || 'notice');
};
const error = (...messages) => {
logs.push(messages.join(' '));
console.log(chalk.red(...messages)); // eslint-disable-line no-console
};
const warn = (...messages) => {
if (logLevel() === 'silent')
return;
logs.push(messages.join(' '));
console.log(chalk.yellow(...messages)); // eslint-disable-line no-console
};
const log = (...messages) => {
if (logLevel() === 'silent' || logLevel() === 'warn')
return;
logs.push(messages.join(' '));
console.log(...messages); // eslint-disable-line no-console
};
const always = (...messages) => {
logs.push(messages.join(' '));
console.log(...messages); // eslint-disable-line no-console
};
// splits long text into lines and calls log()
// on each one to allow easy unit testing for specific message
const logLines = (text) => {
const lines = text.split('\n');
for (const line of lines) {
log(line);
}
};
const print = () => {
return logs.join('\n');
};
const reset = () => {
logs = [];
};
const loggerModule = {
log,
warn,
error,
always,
logLines,
print,
reset,
logLevel,
};
function relativeToRepoRoot(targetPath) {
let currentDir = __dirname;
// Walk up the directory tree
while (currentDir !== path.dirname(currentDir)) {
const resolvedTargetPath = path.join(currentDir, targetPath);
const rootPackageJson = path.join(currentDir, 'package.json');
// Check if this is the `cypress` package.json
if (fs.existsSync(rootPackageJson)) {
try {
const pkg = JSON.parse(fs.readFileSync(rootPackageJson, 'utf8'));
const targetPathExists = fs.existsSync(resolvedTargetPath);
if (targetPathExists && pkg.name === 'cypress') {
return path.resolve(currentDir, targetPath);
}
}
catch (_a) {
// Ignore JSON parse errors
}
}
currentDir = path.dirname(currentDir);
}
return undefined;
}
const debug$2 = Debug('cypress:cli');
const issuesUrl = 'https://github.com/cypress-io/cypress/issues';
/**
* Returns SHA512 of a file
*/
const getFileChecksum = (filename) => {
assert.ok(_.isString(filename) && !_.isEmpty(filename), 'expected filename');
return hasha.fromFile(filename, { algorithm: 'sha512' });
};
const getFileSize = (filename) => __awaiter(void 0, void 0, void 0, function* () {
assert.ok(_.isString(filename) && !_.isEmpty(filename), 'expected filename');
const { size } = yield fs$1.stat(filename);
return size;
});
const isBrokenGtkDisplayRe = /Gtk: cannot open display/;
const stringify = (val) => {
return _.isObject(val) ? JSON.stringify(val) : val;
};
function normalizeModuleOptions(options = {}) {
return _.mapValues(options, stringify);
}
/**
* Returns true if the platform is Linux. We do a lot of different
* stuff on Linux (like Xvfb) and it helps to has readable code
*/
const isLinux = () => {
return os.platform() === 'linux';
};
/**
* If the DISPLAY variable is set incorrectly, when trying to spawn
* Cypress executable we get an error like this:
```
[1005:0509/184205.663837:WARNING:browser_main_loop.cc(258)] Gtk: cannot open display: 99
```
*/
const isBrokenGtkDisplay = (str) => {
return isBrokenGtkDisplayRe.test(str);
};
const isPossibleLinuxWithIncorrectDisplay = () => {
return isLinux() && !!process.env.DISPLAY;
};
const logBrokenGtkDisplayWarning = () => {
debug$2('Cypress exited due to a broken gtk display because of a potential invalid DISPLAY env... retrying after starting Xvfb');
// if we get this error, we are on Linux and DISPLAY is set
loggerModule.warn(commonTags.stripIndent `
${logSymbols.warning} Warning: Cypress failed to start.
This is likely due to a misconfigured DISPLAY environment variable.
DISPLAY was set to: "${process.env.DISPLAY}"
Cypress will attempt to fix the problem and rerun.
`);
loggerModule.warn();
};
function stdoutLineMatches(expectedLine, stdout) {
const lines = stdout.split('\n').map((val) => val.trim());
return lines.some((line) => line === expectedLine);
}
/**
* Confirms if given value is a valid CYPRESS_INTERNAL_ENV value. Undefined values
* are valid, because the system can set the default one.
*
* @param {string} value
* @example util.isValidCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
*/
function isValidCypressInternalEnvValue(value) {
if (_.isUndefined(value)) {
// will get default value
return true;
}
// names of config environments, see "packages/server/config/app.json"
const names = ['development', 'test', 'staging', 'production'];
return _.includes(names, value);
}
/**
* Confirms if given value is a non-production CYPRESS_INTERNAL_ENV value.
* Undefined values are valid, because the system can set the default one.
*
* @param {string} value
* @example util.isNonProductionCypressInternalEnvValue(process.env.CYPRESS_INTERNAL_ENV)
*/
function isNonProductionCypressInternalEnvValue(value) {
return !_.isUndefined(value) && value !== 'production';
}
/**
* Prints NODE_OPTIONS using debug() module, but only
* if DEBUG=cypress... is set
*/
function printNodeOptions(log = debug$2) {
if (!log.enabled) {
return;
}
if (process.env.NODE_OPTIONS) {
log('NODE_OPTIONS=%s', process.env.NODE_OPTIONS);
}
else {
log('NODE_OPTIONS is not set');
}
}
/**
* Removes double quote characters
* from the start and end of the given string IF they are both present
*
* @param {string} str Input string
* @returns {string} Trimmed string or the original string if there are no double quotes around it.
* @example
```
dequote('"foo"')
// returns string 'foo'
dequote('foo')
// returns string 'foo'
```
*/
const dequote = (str) => {
assert.ok(_.isString(str), 'expected a string to remove double quotes');
if (str.length > 1 && str[0] === '"' && str[str.length - 1] === '"') {
return str.substr(1, str.length - 2);
}
return str;
};
const parseOpts = (opts) => {
opts = _.pick(opts, 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', 'cacheClear', 'cachePrune', 'ciBuildId', 'ct', 'component', 'config', 'configFile', 'cypressVersion', 'destination', 'detached', 'dev', 'e2e', 'exit', 'env', 'expose', 'force', 'global', 'group', 'headed', 'headless', 'inspect', 'inspectBrk', 'key', 'path', 'parallel', 'passWithNoTests', 'port', 'posixExitCodes', 'project', 'quiet', 'reporter', 'reporterOptions', 'record', 'runnerUi', 'runProject', 'spec', 'tag');
if (opts.exit) {
opts = _.omit(opts, 'exit');
}
// some options might be quoted - which leads to unexpected results
// remove double quotes from certain options
const cleanOpts = Object.assign({}, opts);
const toDequote = ['group', 'ciBuildId'];
for (const prop of toDequote) {
if (_.has(opts, prop)) {
cleanOpts[prop] = dequote(opts[prop]);
}
}
debug$2('parsed cli options %o', cleanOpts);
return cleanOpts;
};
/**
* Copy of packages/server/lib/browsers/utils.ts
* because we need same functionality in CLI to show the path :(
*/
const getApplicationDataFolder = (...paths) => {
const { env } = process;
// allow overriding the app_data folder
let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development';
// eslint-disable-next-line no-restricted-syntax
const pkg = JSON.parse(fs$1.readFileSync(relativeToRepoRoot('package.json'), 'utf8'));
const PRODUCT_NAME = pkg.productName || pkg.name;
const OS_DATA_PATH = ospath.data();
const ELECTRON_APP_DATA_PATH = path.join(OS_DATA_PATH, PRODUCT_NAME);
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`;
}
const p = path.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths);
return p;
};
const util = {
normalizeModuleOptions,
parseOpts,
isValidCypressInternalEnvValue,
isNonProductionCypressInternalEnvValue,
printNodeOptions,
isCi() {
return ciInfo.isCI;
},
getEnvOverrides(options = {}) {
return _
.chain({})
.extend(this.getEnvColors())
.extend(this.getForceTty())
.omitBy(_.isUndefined) // remove undefined values
.mapValues((value) => {
return value ? '1' : '0';
})
.extend(this.getOriginalNodeOptions())
.value();
},
getOriginalNodeOptions() {
const opts = {};
if (process.env.NODE_OPTIONS) {
opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS;
}
return opts;
},
getForceTty() {
return {
FORCE_STDIN_TTY: this.isTty(process.stdin.fd),
FORCE_STDOUT_TTY: this.isTty(process.stdout.fd),
FORCE_STDERR_TTY: this.isTty(process.stderr.fd),
};
},
getEnvColors() {
const sc = this.supportsColor();
return {
FORCE_COLOR: sc,
DEBUG_COLORS: sc,
MOCHA_COLORS: sc ? true : undefined,
};
},
isTty(fd) {
return tty.isatty(fd);
},
supportsColor() {
// if we've been explicitly told not to support
// color then turn this off
if (process.env.NO_COLOR) {
return false;
}
// https://github.com/cypress-io/cypress/issues/1747
// always return true in CI providers
if (process.env.CI) {
return true;
}
// ensure that both stdout and stderr support color
return Boolean(supportsColor.stdout) && Boolean(supportsColor.stderr);
},
cwd() {
return process$1.cwd();
},
pkgBuildInfo() {
// making this async would require many changes
// eslint-disable-next-line no-restricted-syntax
const pkgContent = fs$1.readFileSync(relativeToRepoRoot('package.json'), 'utf8');
return JSON.parse(pkgContent).buildInfo;
},
pkgVersion() {
// making this async would require many changes
// eslint-disable-next-line no-restricted-syntax
const pkgContent = fs$1.readFileSync(relativeToRepoRoot('package.json'), 'utf8');
return JSON.parse(pkgContent).version;
},
// TODO: remove this method
exit(code) {
process.exit(code);
},
logErrorExit1(err) {
loggerModule.error(err.message);
process.exit(1);
},
dequote,
titleize(...args) {
// prepend first arg with space
// and pad so that all messages line up
args[0] = _.padEnd(` ${args[0]}`, 24);
// get rid of any falsy values
args = _.compact(args);
return chalk.blue(...args);
},
calculateEta(percent, elapsed) {
// returns the number of seconds remaining
// if we're at 100% already just return 0
if (percent === 100) {
return 0;
}
// take the percentage and divide by one
// and multiple that against elapsed
// subtracting what's already elapsed
return elapsed * (1 / (percent / 100)) - elapsed;
},
convertPercentToPercentage(num) {
// convert a percent with values between 0 and 1
// with decimals, so that it is between 0 and 100
// and has no decimal places
return Math.round(_.isFinite(num) ? (num * 100) : 0);
},
secsRemaining(eta) {
// calculate the seconds reminaing with no decimal places
return (_.isFinite(eta) ? (eta / 1000) : 0).toFixed(0);
},
isInstalledGlobally() {
return isInstalledGlobally;
},
isSemver(str) {
return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str);
},
isExecutableAsync(filePath) {
return Promise.resolve(executable(filePath));
},
isLinux,
getOsVersionAsync() {
return __awaiter(this, void 0, void 0, function* () {
try {
const osInfo = yield si.osInfo();
if (osInfo.distro && osInfo.release) {
return `${osInfo.distro} - ${osInfo.release}`;
}
}
catch (err) {
return os.release();
}
return os.release();
});
},
getPlatformInfo() {
return __awaiter(this, void 0, void 0, function* () {
const [version, osArch] = yield Bluebird.all([
this.getOsVersionAsync(),
this.getRealArch(),
]);
return commonTags.stripIndent `
Platform: ${os.platform()}-${osArch} (${version})
Cypress Version: ${this.pkgVersion()}
`;
});
},
_cachedArch: undefined,
/**
* Attempt to return the real system arch (not process.arch, which is only the Node binary's arch)
*/
getRealArch() {
return __awaiter(this, void 0, void 0, function* () {
if (this._cachedArch)
return this._cachedArch;
function _getRealArch() {
return __awaiter(this, void 0, void 0, function* () {
const osPlatform = os.platform();
const osArch = os.arch();
debug$2('detecting arch %o', { osPlatform, osArch });
if (osArch === 'arm64')
return 'arm64';
if (osPlatform === 'darwin') {
// could possibly be x64 node on arm64 darwin, check if we are being translated by Rosetta
// https://stackoverflow.com/a/65347893/3474615
const { stdout } = yield execa('sysctl', ['-n', 'sysctl.proc_translated']).catch(() => ({ stdout: '' }));
debug$2('rosetta check result: %o', { stdout });
if (stdout === '1')
return 'arm64';
}
if (osPlatform === 'linux') {
// could possibly be x64 node on arm64 linux, check the "machine hardware name"
// list of names for reference: https://stackoverflow.com/a/45125525/3474615
const { stdout } = yield execa('uname', ['-m']).catch(() => ({ stdout: '' }));
debug$2('arm uname -m result: %o ', { stdout });
if (['aarch64_be', 'aarch64', 'armv8b', 'armv8l'].includes(stdout))
return 'arm64';
}
const pkgArch = arch();
if (pkgArch === 'x86')
return 'ia32';
return pkgArch;
});
}
return (this._cachedArch = yield _getRealArch());
});
},
// attention:
// when passing relative path to NPM post install hook, the current working
// directory is set to the `node_modules/cypress` folder
// the user is probably passing relative path with respect to root package folder
formAbsolutePath(filename) {
if (path.isAbsolute(filename)) {
return filename;
}
return path.join(process$1.cwd(), '..', '..', filename);
},
getEnv(varName, trim) {
assert.ok(_.isString(varName) && !_.isEmpty(varName), 'expected environment variable name, not');
const configVarName = `npm_config_${varName}`;
const configVarNameLower = configVarName.toLowerCase();
const packageConfigVarName = `npm_package_config_${varName}`;
let result;
if (process.env.hasOwnProperty(varName)) {
debug$2(`Using ${varName} from environment variable`);
result = process.env[varName];
}
else if (process.env.hasOwnProperty(configVarName)) {
debug$2(`Using ${varName} from npm config`);
result = process.env[configVarName];
}
else if (process.env.hasOwnProperty(configVarNameLower)) {
debug$2(`Using ${varName.toLowerCase()} from npm config`);
result = process.env[configVarNameLower];
}
else if (process.env.hasOwnProperty(packageConfigVarName)) {
debug$2(`Using ${varName} from package.json config`);
result = process.env[packageConfigVarName];
}
// environment variables are often set double quotes to escape characters
// and on Windows it can lead to weird things: for example
// set FOO="C:\foo.txt" && node -e "console.log('>>>%s<<<', process.env.FOO)"
// will print
// >>>"C:\foo.txt" <<<
// see https://github.com/cypress-io/cypress/issues/4506#issuecomment-506029942
// so for sanity sake we should first trim whitespace characters and remove
// double quotes around environment strings if the caller is expected to
// use this environment string as a file path
return trim && (result !== null && result !== undefined) ? dequote(_.trim(result)) : result;
},
getCacheDir() {
return cachedir('Cypress');
},
isPostInstall() {
return process.env.npm_lifecycle_event === 'postinstall';
},
exec: execa,
stdoutLineMatches,
issuesUrl,
isBrokenGtkDisplay,
logBrokenGtkDisplayWarning,
isPossibleLinuxWithIncorrectDisplay,
getGitHubIssueUrl(number) {
assert.ok(_.isInteger(number), 'github issue should be an integer');
assert.ok(number > 0, 'github issue should be a positive number');
return `${issuesUrl}/${number}`;
},
getFileChecksum,
getFileSize,
getApplicationDataFolder,
};
const debug$1 = Debug('cypress:cli');
const getPlatformExecutable = () => {
const platform = os.platform();
switch (platform) {
case 'darwin': return 'Contents/MacOS/Cypress';
case 'linux': return 'Cypress';
case 'win32': return 'Cypress.exe';
// TODO handle this error using our standard
default: throw new Error(`Platform: "${platform}" is not supported.`);
}
};
const getPlatFormBinaryFolder = () => {
const platform = os.platform();
switch (platform) {
case 'darwin': return 'Cypress.app';
case 'linux': return 'Cypress';
case 'win32': return 'Cypress';
// TODO handle this error using our standard
default: throw new Error(`Platform: "${platform}" is not supported.`);
}
};
const getBinaryPkgPath = (binaryDir) => {
const platform = os.platform();
switch (platform) {
case 'darwin': return path.join(binaryDir, 'Contents', 'Resources', 'app', 'package.json');
case 'linux': return path.join(binaryDir, 'resources', 'app', 'package.json');
case 'win32': return path.join(binaryDir, 'resources', 'app', 'package.json');
// TODO handle this error using our standard
default: throw new Error(`Platform: "${platform}" is not supported.`);
}
};
/**
* Get path to binary directory
*/
const getBinaryDir = (version = util.pkgVersion()) => {
return path.join(getVersionDir(version), getPlatFormBinaryFolder());
};
const getVersionDir = (version = util.pkgVersion(), buildInfo = util.pkgBuildInfo()) => {
if (buildInfo && !buildInfo.stable) {
version = ['beta', version, buildInfo.commitBranch, buildInfo.commitSha.slice(0, 8)].join('-');
}
return path.join(getCacheDir(), version);
};
/**
* When executing "npm postinstall" hook, the working directory is set to
* "<current folder>/node_modules/cypress", which can be surprising when using relative paths.
*/
const isInstallingFromPostinstallHook = () => {
// individual folders
const cwdFolders = process$1.cwd().split(path.sep);
const length = cwdFolders.length;
return cwdFolders[length - 2] === 'node_modules' && cwdFolders[length - 1] === 'cypress';
};
const getCacheDir = () => {
let cache_directory = util.getCacheDir();
if (util.getEnv('CYPRESS_CACHE_FOLDER')) {
const envVarCacheDir = untildify(util.getEnv('CYPRESS_CACHE_FOLDER'));
debug$1('using environment variable CYPRESS_CACHE_FOLDER %s', envVarCacheDir);
if (!path.isAbsolute(envVarCacheDir) && isInstallingFromPostinstallHook()) {
const packageRootFolder = path.join('..', '..', envVarCacheDir);
cache_directory = path.resolve(packageRootFolder);
debug$1('installing from postinstall hook, original root folder is %s', packageRootFolder);
debug$1('and resolved cache directory is %s', cache_directory);
}
else {
cache_directory = path.resolve(envVarCacheDir);
}
}
return cache_directory;
};
const parseRealPlatformBinaryFolderAsync = (binaryPath) => __awaiter(void 0, void 0, void 0, function* () {
const realPath = yield fs$1.realpath(binaryPath);
debug$1('CYPRESS_RUN_BINARY has realpath:', realPath);
if (!realPath.toString().endsWith(getPlatformExecutable())) {
return false;
}
if (os.platform() === 'darwin') {
return path.resolve(realPath, '..', '..', '..');
}
return path.resolve(realPath, '..');
});
const getDistDir = () => {
return path.join(__dirname, '..', '..', 'dist');
};
/**
* Returns full filename to the file that keeps the Test Runner verification state as JSON text.
* Note: the binary state file will be stored one level up from the given binary folder.
* @param {string} binaryDir - full path to the folder holding the binary.
*/
const getBinaryStatePath = (binaryDir) => {
return path.join(binaryDir, '..', 'binary_state.json');
};
const getBinaryStateContentsAsync = (binaryDir) => __awaiter(void 0, void 0, void 0, function* () {
const fullPath = getBinaryStatePath(binaryDir);
try {
const contents = yield fs$1.readJson(fullPath);
debug$1('binary_state.json contents:', contents);
return contents;
}
catch (error) {
if (error.code === 'ENOENT' || error instanceof SyntaxError) {
debug$1('could not read binary_state.json file at "%s"', fullPath);
return {};
}
throw error;
}
});
const getBinaryVerifiedAsync = (binaryDir) => __awaiter(void 0, void 0, void 0, function* () {
const contents = yield getBinaryStateContentsAsync(binaryDir);
return contents.verified;
});
const clearBinaryStateAsync = (binaryDir) => __awaiter(void 0, void 0, void 0, function* () {
yield fs$1.remove(getBinaryStatePath(binaryDir));
});
/**
* Writes the new binary status.
* @param {boolean} verified The new test runner state after smoke test
* @param {string} binaryDir Folder holding the binary
* @returns {Promise<void>} returns a promise
*/
const writeBinaryVerifiedAsync = (verified, binaryDir) => __awaiter(void 0, void 0, void 0, function* () {
const contents = yield getBinaryStateContentsAsync(binaryDir);
yield fs$1.outputJson(getBinaryStatePath(binaryDir), _.extend(contents, { verified }), { spaces: 2 });
});
const getPathToExecutable = (binaryDir) => {
return path.join(binaryDir, getPlatformExecutable());
};
/**
* Resolves with an object read from the binary app package.json file.
* If the file does not exist resolves with null
*/
const getBinaryPkgAsync = (binaryDir) => __awaiter(void 0, void 0, void 0, function* () {
const pathToPackageJson = getBinaryPkgPath(binaryDir);
debug$1('Reading binary package.json from:', pathToPackageJson);
const exists = yield fs$1.pathExists(pathToPackageJson);
if (!exists) {
return null;
}
return fs$1.readJson(pathToPackageJson);
});
const getBinaryPkgVersion = (o) => _.get(o, 'version', null);
const getBinaryElectronVersion = (o) => _.get(o, 'electronVersion', null);
const getBinaryElectronNodeVersion = (o) => _.get(o, 'electronNodeVersion', null);
const stateModule = {
getPathToExecutable,
getPlatformExecutable,
// those names start to sound like Java
getBinaryElectronNodeVersion,
getBinaryElectronVersion,
getBinaryPkgVersion,
getBinaryVerifiedAsync,
getBinaryPkgAsync,
getBinaryPkgPath,
getBinaryDir,
getCacheDir,
clearBinaryStateAsync,
writeBinaryVerifiedAsync,
parseRealPlatformBinaryFolderAsync,
getDistDir,
getVersionDir,
};
const docsUrl = 'https://on.cypress.io';
const requiredDependenciesUrl = `${docsUrl}/required-dependencies`;
const runDocumentationUrl = `${docsUrl}/cypress-run`;
// TODO it would be nice if all error objects could be enforced via types
// to only have description + solution properties
const hr = '----------';
const genericErrorSolution = commonTags.stripIndent `
Search for an existing issue or open a GitHub issue at
${chalk.blue(util.issuesUrl)}
`;
// common errors Cypress application can encounter
const unknownError = {
description: 'Unknown Cypress CLI error',
solution: genericErrorSolution,
};
const invalidRunProjectPath = {
description: 'Invalid --project path',
solution: commonTags.stripIndent `
Please provide a valid project path.
Learn more about ${chalk.cyan('cypress run')} at:
${chalk.blue(runDocumentationUrl)}
`,
};
const invalidOS = {
description: 'The Cypress App could not be installed. Your machine does not meet the operating system requirements.',
solution: commonTags.stripIndent `
${chalk.blue('https://on.cypress.io/app/get-started/install-cypress#System-requirements')}`,
};
const failedDownload = {
description: 'The Cypress App could not be downloaded.',
solution: commonTags.stripIndent `
Does your workplace require a proxy to be used to access the Internet? If so, you must configure the HTTP_PROXY environment variable before downloading Cypress. Read more: https://on.cypress.io/proxy-configuration
Otherwise, please check network connectivity and try again:`,
};
const failedUnzip = {
description: 'The Cypress App could not be unzipped.',
solution: genericErrorSolution,
};
const failedUnzipWindowsMaxPathLength = {
description: 'The Cypress App could not be unzipped.',
solution: `This is most likely because the maximum path length is being exceeded on your system.
Read here for solutions to this problem: https://on.cypress.io/win-max-path-length-error`,
};
const missingApp = (binaryDir) => {
return {
description: `No version of Cypress is installed in: ${chalk.cyan(binaryDir)}`,
solution: commonTags.stripIndent `
\nPlease reinstall Cypress by running: ${chalk.cyan('cypress install')}
`,
};
};
const binaryNotExecutable = (executable) => {
return {
description: `Cypress cannot run because this binary file does not have executable permissions here:\n\n${executable}`,
solution: commonTags.stripIndent `\n
Reasons this may happen:
- node was installed as 'root' or with 'sudo'
- the cypress npm package as 'root' or with 'sudo'
Please check that you have the appropriate user permissions.
You can also try clearing the cache with 'cypress cache clear' and reinstalling.
`,
};
};
const notInstalledCI = (executable) => {
return {
description: 'The cypress npm package is installed, but the Cypress binary is missing.',
solution: commonTags.stripIndent `\n
We expected the binary to be installed here: ${chalk.cyan(executable)}
Reasons it may be missing:
- You're caching 'node_modules' but are not caching this path: ${util.getCacheDir()}
- You ran 'npm install' at an earlier build step but did not persist: ${util.getCacheDir()}
Properly caching the binary will fix this error and avoid downloading and unzipping Cypress.
Alternatively, you can run 'cypress install' to download the binary again.
${chalk.blue('https://on.cypress.io/not-installed-ci-error')}
`,
};
};
const nonZeroExitCodeXvfb = {
description: 'Xvfb exited with a non zero exit code.',
solution: commonTags.stripIndent `
There was a problem spawning Xvfb.
This is likely a problem with your system, permissions, or installation of Xvfb.
`,
};
const missingXvfb = {
description: 'Your system is missing the dependency: Xvfb',
solution: commonTags.stripIndent `
Install Xvfb and run Cypress again.
Read our documentation on dependencies for more information:
${chalk.blue(requiredDependenciesUrl)}
If you are using Docker, we provide containers with all required dependencies installed.
`,
};
const smokeTestFailure = (smokeTestCommand, timedOut) => {
return {
description: `Cypress verification ${timedOut ? 'timed out' : 'failed'}.`,
solution: commonTags.stripIndent `
This command failed with the following output:
${smokeTestCommand}
`,
};
};
const invalidSmokeTestDisplayError = {
code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR',
description: 'Cypress verification failed.',
solution(msg) {
return commonTags.stripIndent `
Cypress failed to start after spawning a new Xvfb server.
The error logs we received were:
${hr}
${msg}
${hr}
This may be due to a missing library or dependency. ${chalk.blue(requiredDependenciesUrl)}
Please refer to the error above for more detail.
`;
},
};
const missingDependency = {
description: 'Cypress failed to start.',
// this message is too Linux specific
solution: commonTags.stripIndent `
This may be due to a missing library or dependency. ${chalk.blue(requiredDependenciesUrl)}
Please refer to the error below for more details.
`,
};
const invalidCacheDirectory = {
description: 'Cypress cannot write to the cache directory due to file permissions',
solution: commonTags.stripIndent `
See discussion and possible solutions at
${chalk.blue(util.getGitHubIssueUrl(1281))}
`,
};
const versionMismatch = {
description: 'Installed version does not match package version.',
solution: 'Install Cypress and verify app again',
};
const incompatibleHeadlessFlags = {
description: '`--headed` and `--headless` cannot both be passed.',
solution: 'Either pass `--headed` or `--headless`, but not both.',
};
const solutionUnknown = commonTags.stripIndent `
Please search Cypress documentation for possible solutions:
${chalk.blue(docsUrl)}
Check if there is a GitHub issue describing this crash:
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`;
const unexpected = {
description: 'An unexpected error occurred while verifying the Cypress executable.',
solution: solutionUnknown,
};
const invalidCypressEnv = {
description: chalk.red('The environment variable with the reserved name "CYPRESS_INTERNAL_ENV" is set.'),
solution: chalk.red('Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again.'),
exitCode: 11,
};
const invalidTestingType = {
description: 'Invalid testingType',
solution: `Please provide a valid testingType. Valid test types are ${chalk.cyan('\'e2e\'')} and ${chalk.cyan('\'component\'')}.`,
};
const incompatibleTestTypeFlags = {
description: '`--e2e` and `--component` cannot both be passed.',
solution: 'Either pass `--e2e` or `--component`, but not both.',
};
const incompatibleTestingTypeAndFlag = {
description: 'Set a `testingType` and also passed `--e2e` or `--component` flags.',
solution: 'Either set `testingType` or pass a testing type flag, but not both.',
};
const invalidConfigFile = {
description: '`--config-file` cannot be false.',
solution: 'Either pass a relative path to a valid Cypress config file or remove this option.',
};
/**
* This error happens when CLI detects that the child Test Runner process
* was killed with a signal, like SIGBUS
* @see https://github.com/cypress-io/cypress/issues/5808
* @param {'close'|'event'} eventName Child close event name
* @param {string} signal Signal that closed the child process, like "SIGBUS"
*/
const childProcessKilled = (eventName, signal) => {
return {
description: `The Test Runner unexpectedly exited via a ${chalk.cyan(eventName)} event with signal ${chalk.cyan(signal)}`,
solution: solutionUnknown,
};
};
const CYPRESS_RUN_BINARY = {
notValid: (value) => {
const properFormat = `**/${stateModule.getPlatformExecutable()}`;
return {
description: `Could not run binary set by environment variable: CYPRESS_RUN_BINARY=${value}`,
solution: `Ensure the environment variable is a path to the Cypress binary, matching ${properFormat}`,
};
},
};
function getErrorSync(errorObject, platform) {
const errorMessage = syncFormErrorText(Object.assign(Object.assign({}, errorObject), { platform }));
const err = new Error(errorMessage);
err.known = true;
return err;
}
function formErrorText(info, msg, prevMessage) {
return __awaiter(this, void 0, void 0, function* () {
const platform = yield util.getPlatformInfo();
return syncFormErrorText(Object.assign(Object.assign({}, info), { platform }), msg, prevMessage);
});
}
function syncFormErrorText(info, msg, prevMessage) {
const formatted = [];
function add(msg) {
formatted.push(commonTags.stripIndents(msg));
}
assert.ok(_.isString(info.description) && !_.isEmpty(info.description), 'expected error description to be text.');
// assuming that if there the solution is a function it will handle
// error message and (optional previous error message)
if (_.isFunction(info.solution)) {
const text = info.solution(msg, prevMessage);
assert.ok(_.isString(text) && !_.isEmpty(text), 'expected solution to be text.');
add(`
${info.description}
${text}
`);
}
else {
assert.ok(_.isString(info.solution) && !_.isEmpty(info.solution), 'expected error solution to be text.');
add(`
${info.description}
${info.solution}
`);
if (msg) {
add(`
${hr}
${msg}
`);
}
}
add(`
${hr}
${info.platform}
`);
if (info.footer) {
add(`
${hr}
${info.footer}
`);
}
return formatted.join('\n\n');
}
const raise = (info) => {
return (text) => {
const err = new Error(text);
if (info.code) {
err.code = info.code;
}
err.known = true;
throw err;
};
};
const throwFormErrorText = (info) => {
return (msg, prevMessage) => __awaiter(void 0, void 0, void 0, function* () {
const errorText = yield formErrorText(info, msg, prevMessage);
raise(info)(errorText);
});
};
/**
* Forms full error message with error and OS details, prints to the error output
* and then exits the process.
* @param {ErrorInformation} info Error information {description, solution}
* @example return exitWithError(errors.invalidCypressEnv)('foo')
*/
const exitWithError = (info) => {
return (msg) => __awaiter(void 0, void 0, void 0, function* () {
const text = yield formErrorText(info, msg);
console.error(text);
process.exit(info.exitCode || 1);
});
};
const errors = {
unknownError,
nonZeroExitCodeXvfb,
missingXvfb,
missingApp,
notInstalledCI,
missingDependency,
invalidOS,
invalidSmokeTestDisplayError,
versionMismatch,
binaryNotExecutable,
unexpected,
failedDownload,
failedUnzip,
failedUnzipWindowsMaxPathLength,
invalidCypressEnv,
invalidCacheDirectory,
CYPRESS_RUN_BINARY,
smokeTestFailure,
childProcessKilled,
incompatibleHeadlessFlags,
invalidRunProjectPath,
invalidTestingType,
incompatibleTestTypeFlags,
incompatibleTestingTypeAndFlag,
invalidConfigFile,
};
const debug = Debug('cypress:cli');
const debugXvfb = Debug('cypress:xvfb');
debug.Debug = debugXvfb.Debug = Debug;
const xvfbOptions = {
displayNum: process.env.XVFB_DISPLAY_NUM,
timeout: 30000, // milliseconds
// need to explicitly define screen otherwise electron will crash
// https://github.com/cypress-io/cypress/issues/6184
xvfb_args: ['-screen', '0', '1280x1024x24'],
onStderrData(data) {
if (debugXvfb.enabled) {
debugXvfb(data.toString());
}
},
};
const xvfb = Bluebird.promisifyAll(new Xvfb(xvfbOptions));
const _debugXvfb = debugXvfb;
const _xvfb = xvfb;
const _xvfbOptions = xvfbOptions;
function start() {
return __awaiter(this, void 0, void 0, function* () {
debug('Starting Xvfb');
try {
yield xvfb.startAsync();
return null;
}
catch (e) {
if (e.nonZeroExitCode === true) {
const raiseErrorFn = throwFormErrorText(errors.nonZeroExitCodeXvfb);
yield raiseErrorFn(e);
}
if (e.known) {
throw e;
}
const raiseErrorFn = throwFormErrorText(errors.missingXvfb);
yield raiseErrorFn(e);
}
});
}
function stop() {
return __awaiter(this, void 0, void 0, function* () {
debug('Stopping Xvfb');
try {
yield xvfb.stopAsync();
return null;
}
catch (e) {
return null;
}
});
}
function isNeeded() {
if (process.env.ELECTRON_RUN_AS_NODE) {
debug('Environment variable ELECTRON_RUN_AS_NODE detected, xvfb is not needed');
return false; // xvfb required for electron processes only.
}
if (os.platform() !== 'linux') {
return false;
}
if (process.env.DISPLAY) {
const issueUrl = util.getGitHubIssueUrl(4034);
const message = commonTags.stripIndent `
DISPLAY environment variable is set to ${process.env.DISPLAY} on Linux
Assuming this DISPLAY points at working X11 server,
Cypress will not spawn own Xvfb
NOTE: if the X11 server is NOT working, Cypress will exit without explanation,
see ${issueUrl}
Solution: Unset the DISPLAY variable and try again:
DISPLAY= npx cypress run ...
`;
debug(message);
return false;
}
debug('undefined DISPLAY environment variable');
debug('Cypress will spawn its own Xvfb');
return true;
}
// async method, resolved with Boolean
function verify() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield xvfb.startAsync();
return true;
}
catch (err) {
debug('Could not verify xvfb: %s', err.message);
return false;
}
finally {
yield xvfb.stopAsync();
}
});
}
var xvfb$1 = {
_debugXvfb,
_xvfb,
_xvfbOptions,
start,
stop,
isNeeded,
verify,
};
exports.__awaiter = __awaiter;
exports._debugXvfb = _debugXvfb;
exports._xvfb = _xvfb;
exports._xvfbOptions = _xvfbOptions;
exports.errors = errors;
exports.exitWithError = exitWithError;
exports.getErrorSync = getErrorSync;
exports.isNeeded = isNeeded;
exports.loggerModule = loggerModule;
exports.relativeToRepoRoot = relativeToRepoRoot;
exports.start = start;
exports.stateModule = stateModule;
exports.stop = stop;
exports.throwFormErrorText = throwFormErrorText;
exports.util = util;
exports.verify = verify;
exports.xvfb = xvfb$1;