UNPKG

cypress

Version:

Cypress is a next generation front end testing tool built for the modern web

451 lines (446 loc) 18.5 kB
"use strict"; 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;