cypress
Version:
Cypress is a next generation front end testing tool built for the modern web
451 lines (446 loc) • 18.5 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = __importDefault(require("lodash"));
const assert_1 = __importDefault(require("assert"));
const arch_1 = __importDefault(require("arch"));
const os_1 = __importDefault(require("os"));
const ospath_1 = __importDefault(require("ospath"));
const hasha_1 = __importDefault(require("hasha"));
const tty_1 = __importDefault(require("tty"));
const path_1 = __importDefault(require("path"));
const ci_info_1 = require("ci-info");
const execa_1 = __importDefault(require("execa"));
const systeminformation_1 = __importDefault(require("systeminformation"));
const chalk_1 = __importDefault(require("chalk"));
const bluebird_1 = __importDefault(require("bluebird"));
const cachedir_1 = __importDefault(require("cachedir"));
const log_symbols_1 = __importDefault(require("log-symbols"));
const executable_1 = __importDefault(require("executable"));
const process_1 = require("process");
const common_tags_1 = require("common-tags");
const supports_color_1 = __importDefault(require("supports-color"));
const is_installed_globally_1 = __importDefault(require("is-installed-globally"));
const logger_1 = __importDefault(require("./logger"));
const debug_1 = __importDefault(require("debug"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const package_json_1 = __importDefault(require("../package.json"));
const debug = (0, debug_1.default)('cypress:cli');
const issuesUrl = 'https://github.com/cypress-io/cypress/issues';
/**
* Returns SHA512 of a file
*/
const getFileChecksum = (filename) => {
assert_1.default.ok(lodash_1.default.isString(filename) && !lodash_1.default.isEmpty(filename), 'expected filename');
return hasha_1.default.fromFile(filename, { algorithm: 'sha512' });
};
const getFileSize = (filename) => __awaiter(void 0, void 0, void 0, function* () {
assert_1.default.ok(lodash_1.default.isString(filename) && !lodash_1.default.isEmpty(filename), 'expected filename');
const { size } = yield fs_extra_1.default.stat(filename);
return size;
});
const isBrokenGtkDisplayRe = /Gtk: cannot open display/;
const stringify = (val) => {
return lodash_1.default.isObject(val) ? JSON.stringify(val) : val;
};
function normalizeModuleOptions(options = {}) {
return lodash_1.default.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_1.default.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('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
logger_1.default.warn((0, common_tags_1.stripIndent) `
${log_symbols_1.default.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.
`);
logger_1.default.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 (lodash_1.default.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 lodash_1.default.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 !lodash_1.default.isUndefined(value) && value !== 'production';
}
/**
* Prints NODE_OPTIONS using debug() module, but only
* if DEBUG=cypress... is set
*/
function printNodeOptions(log = debug) {
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_1.default.ok(lodash_1.default.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 = lodash_1.default.pick(opts, 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', 'cacheClear', 'cachePrune', 'ciBuildId', 'ct', 'component', 'config', 'configFile', 'cypressVersion', 'destination', 'detached', 'dev', 'e2e', 'exit', 'env', 'force', 'global', 'group', 'headed', 'headless', 'inspect', 'inspectBrk', 'key', 'path', 'parallel', 'port', 'posixExitCodes', 'project', 'quiet', 'reporter', 'reporterOptions', 'record', 'runnerUi', 'runProject', 'spec', 'tag');
if (opts.exit) {
opts = lodash_1.default.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 (lodash_1.default.has(opts, prop)) {
cleanOpts[prop] = dequote(opts[prop]);
}
}
debug('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';
// @ts-expect-error value exists but is not typed
const PRODUCT_NAME = package_json_1.default.productName || package_json_1.default.name;
const OS_DATA_PATH = ospath_1.default.data();
const ELECTRON_APP_DATA_PATH = path_1.default.join(OS_DATA_PATH, PRODUCT_NAME);
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF) {
folder = `${folder}-e2e-test`;
}
const p = path_1.default.join(ELECTRON_APP_DATA_PATH, 'cy', folder, ...paths);
return p;
};
const util = {
normalizeModuleOptions,
parseOpts,
isValidCypressInternalEnvValue,
isNonProductionCypressInternalEnvValue,
printNodeOptions,
isCi() {
return ci_info_1.isCI;
},
getEnvOverrides(options = {}) {
return lodash_1.default
.chain({})
.extend(this.getEnvColors())
.extend(this.getForceTty())
.omitBy(lodash_1.default.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_1.default.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(supports_color_1.default.stdout) && Boolean(supports_color_1.default.stderr);
},
cwd() {
return (0, process_1.cwd)();
},
pkgBuildInfo() {
// @ts-expect-error value exists but is not typed
return package_json_1.default.buildInfo;
},
pkgVersion() {
return package_json_1.default.version;
},
// TODO: remove this method
exit(code) {
process.exit(code);
},
logErrorExit1(err) {
logger_1.default.error(err.message);
process.exit(1);
},
dequote,
titleize(...args) {
// prepend first arg with space
// and pad so that all messages line up
args[0] = lodash_1.default.padEnd(` ${args[0]}`, 24);
// get rid of any falsy values
args = lodash_1.default.compact(args);
return chalk_1.default.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(lodash_1.default.isFinite(num) ? (num * 100) : 0);
},
secsRemaining(eta) {
// calculate the seconds reminaing with no decimal places
return (lodash_1.default.isFinite(eta) ? (eta / 1000) : 0).toFixed(0);
},
setTaskTitle(task, title, renderer) {
// only update the renderer title when not running in CI
if (renderer === 'default' && task.title !== title) {
task.title = title;
}
},
isInstalledGlobally() {
return is_installed_globally_1.default;
},
isSemver(str) {
return /^(\d+\.)?(\d+\.)?(\*|\d+)$/.test(str);
},
isExecutableAsync(filePath) {
return bluebird_1.default.resolve((0, executable_1.default)(filePath));
},
isLinux,
getOsVersionAsync() {
return __awaiter(this, void 0, void 0, function* () {
try {
const osInfo = yield systeminformation_1.default.osInfo();
if (osInfo.distro && osInfo.release) {
return `${osInfo.distro} - ${osInfo.release}`;
}
}
catch (err) {
return os_1.default.release();
}
return os_1.default.release();
});
},
getPlatformInfo() {
return __awaiter(this, void 0, void 0, function* () {
const [version, osArch] = yield bluebird_1.default.all([
this.getOsVersionAsync(),
this.getRealArch(),
]);
return (0, common_tags_1.stripIndent) `
Platform: ${os_1.default.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_1.default.platform();
const osArch = os_1.default.arch();
debug('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 (0, execa_1.default)('sysctl', ['-n', 'sysctl.proc_translated']).catch(() => ({ stdout: '' }));
debug('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 (0, execa_1.default)('uname', ['-m']).catch(() => ({ stdout: '' }));
debug('arm uname -m result: %o ', { stdout });
if (['aarch64_be', 'aarch64', 'armv8b', 'armv8l'].includes(stdout))
return 'arm64';
}
const pkgArch = (0, arch_1.default)();
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_1.default.isAbsolute(filename)) {
return filename;
}
return path_1.default.join((0, process_1.cwd)(), '..', '..', filename);
},
getEnv(varName, trim) {
assert_1.default.ok(lodash_1.default.isString(varName) && !lodash_1.default.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(`Using ${varName} from environment variable`);
result = process.env[varName];
}
else if (process.env.hasOwnProperty(configVarName)) {
debug(`Using ${varName} from npm config`);
result = process.env[configVarName];
}
else if (process.env.hasOwnProperty(configVarNameLower)) {
debug(`Using ${varName.toLowerCase()} from npm config`);
result = process.env[configVarNameLower];
}
else if (process.env.hasOwnProperty(packageConfigVarName)) {
debug(`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(lodash_1.default.trim(result)) : result;
},
getCacheDir() {
return (0, cachedir_1.default)('Cypress');
},
isPostInstall() {
return process.env.npm_lifecycle_event === 'postinstall';
},
exec: execa_1.default,
stdoutLineMatches,
issuesUrl,
isBrokenGtkDisplay,
logBrokenGtkDisplayWarning,
isPossibleLinuxWithIncorrectDisplay,
getGitHubIssueUrl(number) {
assert_1.default.ok(lodash_1.default.isInteger(number), 'github issue should be an integer');
assert_1.default.ok(number > 0, 'github issue should be a positive number');
return `${issuesUrl}/${number}`;
},
getFileChecksum,
getFileSize,
getApplicationDataFolder,
};
exports.default = util;
;