UNPKG

cypress

Version:

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

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