|
"use strict"; |
|
|
|
|
|
const _ = require('lodash'); |
|
const commander = require('commander'); |
|
const { |
|
stripIndent |
|
} = require('common-tags'); |
|
const logSymbols = require('log-symbols'); |
|
const debug = require('debug')('cypress:cli:cli'); |
|
const util = require('./util'); |
|
const logger = require('./logger'); |
|
const errors = require('./errors'); |
|
const cache = require('./tasks/cache'); |
|
|
|
|
|
|
|
function unknownOption(flag, type = 'option') { |
|
if (this._allowUnknownOption) return; |
|
logger.error(); |
|
logger.error(` error: unknown ${type}:`, flag); |
|
logger.error(); |
|
this.outputHelp(); |
|
util.exit(1); |
|
} |
|
commander.Command.prototype.unknownOption = unknownOption; |
|
const coerceFalse = arg => { |
|
return arg !== 'false'; |
|
}; |
|
const coerceAnyStringToInt = arg => { |
|
return typeof arg === 'string' ? parseInt(arg) : arg; |
|
}; |
|
const spaceDelimitedArgsMsg = (flag, args) => { |
|
let msg = ` |
|
${logSymbols.warning} Warning: It looks like you're passing --${flag} a space-separated list of arguments: |
|
|
|
"${args.join(' ')}" |
|
|
|
This will work, but it's not recommended. |
|
|
|
If you are trying to pass multiple arguments, separate them with commas instead: |
|
cypress run --${flag} arg1,arg2,arg3 |
|
`; |
|
if (flag === 'spec') { |
|
msg += ` |
|
The most common cause of this warning is using an unescaped glob pattern. If you are |
|
trying to pass a glob pattern, escape it using quotes: |
|
cypress run --spec "**/*.spec.js" |
|
`; |
|
} |
|
logger.log(); |
|
logger.warn(stripIndent(msg)); |
|
logger.log(); |
|
}; |
|
const parseVariableOpts = (fnArgs, args) => { |
|
const [opts, unknownArgs] = fnArgs; |
|
if (unknownArgs && unknownArgs.length && (opts.spec || opts.tag)) { |
|
|
|
|
|
|
|
|
|
|
|
const multiArgFlags = _.compact([opts.spec ? 'spec' : opts.spec, opts.tag ? 'tag' : opts.tag]); |
|
_.forEach(multiArgFlags, flag => { |
|
const argIndex = _.indexOf(args, `--${flag}`) + 2; |
|
const nextOptOffset = _.findIndex(_.slice(args, argIndex), arg => { |
|
return _.startsWith(arg, '--'); |
|
}); |
|
const endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length; |
|
const maybeArgs = _.slice(args, argIndex, endIndex); |
|
const extraArgs = _.intersection(maybeArgs, unknownArgs); |
|
if (extraArgs.length) { |
|
opts[flag] = [opts[flag]].concat(extraArgs); |
|
spaceDelimitedArgsMsg(flag, opts[flag]); |
|
opts[flag] = opts[flag].join(','); |
|
} |
|
}); |
|
} |
|
debug('variable-length opts parsed %o', { |
|
args, |
|
opts |
|
}); |
|
return util.parseOpts(opts); |
|
}; |
|
const descriptions = { |
|
autoCancelAfterFailures: 'overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud', |
|
browser: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.', |
|
cacheClear: 'delete all cached binaries', |
|
cachePrune: 'deletes all cached binaries except for the version currently in use', |
|
cacheList: 'list cached binary versions', |
|
cachePath: 'print the path to the binary cache', |
|
cacheSize: 'Used with the list command to show the sizes of the cached folders', |
|
ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers', |
|
component: 'runs component tests', |
|
config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}.', |
|
configFile: 'path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}".', |
|
detached: 'runs Cypress application in detached mode', |
|
dev: 'runs cypress in development and bypasses binary check', |
|
e2e: 'runs end to end tests', |
|
env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json', |
|
exit: 'keep the browser open after tests finish', |
|
forceInstall: 'force install the Cypress binary', |
|
global: 'force Cypress into global mode as if its globally installed', |
|
group: 'a named group for recorded runs in Cypress Cloud', |
|
headed: 'displays the browser instead of running headlessly', |
|
headless: 'hide the browser instead of running headed (default for cypress run)', |
|
key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.', |
|
parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes', |
|
port: 'runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}.', |
|
project: 'path to the project', |
|
quiet: 'run quietly, using only the configured reporter', |
|
record: 'records the run. sends test results, screenshots and videos to Cypress Cloud.', |
|
reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"', |
|
reporterOptions: 'options for the mocha reporter. defaults to "null"', |
|
runnerUi: 'displays the Cypress Runner UI', |
|
noRunnerUi: 'hides the Cypress Runner UI', |
|
spec: 'runs specific spec file(s). defaults to "all"', |
|
tag: 'named tag(s) for recorded runs in Cypress Cloud', |
|
version: 'prints Cypress version' |
|
}; |
|
const knownCommands = ['cache', 'help', '-h', '--help', 'install', 'open', 'run', 'open-ct', 'run-ct', 'verify', '-v', '--version', 'version', 'info']; |
|
const text = description => { |
|
if (!descriptions[description]) { |
|
throw new Error(`Could not find description for: ${description}`); |
|
} |
|
return descriptions[description]; |
|
}; |
|
function includesVersion(args) { |
|
return _.includes(args, '--version') || _.includes(args, '-v'); |
|
} |
|
function showVersions(opts) { |
|
debug('printing Cypress version'); |
|
debug('additional arguments %o', opts); |
|
debug('parsed version arguments %o', opts); |
|
const reportAllVersions = versions => { |
|
logger.always('Cypress package version:', versions.package); |
|
logger.always('Cypress binary version:', versions.binary); |
|
logger.always('Electron version:', versions.electronVersion); |
|
logger.always('Bundled Node version:', versions.electronNodeVersion); |
|
}; |
|
const reportComponentVersion = (componentName, versions) => { |
|
const names = { |
|
package: 'package', |
|
binary: 'binary', |
|
electron: 'electronVersion', |
|
node: 'electronNodeVersion' |
|
}; |
|
if (!names[componentName]) { |
|
throw new Error(`Unknown component name "${componentName}"`); |
|
} |
|
const name = names[componentName]; |
|
if (!versions[name]) { |
|
throw new Error(`Cannot find version for component "${componentName}" under property "${name}"`); |
|
} |
|
const version = versions[name]; |
|
logger.always(version); |
|
}; |
|
const defaultVersions = { |
|
package: undefined, |
|
binary: undefined, |
|
electronVersion: undefined, |
|
electronNodeVersion: undefined |
|
}; |
|
return require('./exec/versions').getVersions().then((versions = defaultVersions) => { |
|
if (opts !== null && opts !== void 0 && opts.component) { |
|
reportComponentVersion(opts.component, versions); |
|
} else { |
|
reportAllVersions(versions); |
|
} |
|
process.exit(0); |
|
}).catch(util.logErrorExit1); |
|
} |
|
const createProgram = () => { |
|
const program = new commander.Command(); |
|
|
|
|
|
|
|
program._name = 'cypress'; |
|
program.usage('<command> [options]'); |
|
return program; |
|
}; |
|
const addCypressRunCommand = program => { |
|
return program.command('run').usage('[options]').description('Runs Cypress tests from the CLI without the GUI').option('--auto-cancel-after-failures <test-failure-count || false>', text('autoCancelAfterFailures')).option('-b, --browser <browser-name-or-path>', text('browser')).option('--ci-build-id <id>', text('ciBuildId')).option('--component', text('component')).option('-c, --config <config>', text('config')).option('-C, --config-file <config-file>', text('configFile')).option('--e2e', text('e2e')).option('-e, --env <env>', text('env')).option('--group <name>', text('group')).option('-k, --key <record-key>', text('key')).option('--headed', text('headed')).option('--headless', text('headless')).option('--no-exit', text('exit')).option('--parallel', text('parallel')).option('-p, --port <port>', text('port')).option('-P, --project <project-path>', text('project')).option('-q, --quiet', text('quiet')).option('--record [bool]', text('record'), coerceFalse).option('-r, --reporter <reporter>', text('reporter')).option('--runner-ui', text('runnerUi')).option('--no-runner-ui', text('noRunnerUi')).option('-o, --reporter-options <reporter-options>', text('reporterOptions')).option('-s, --spec <spec>', text('spec')).option('-t, --tag <tag>', text('tag')).option('--dev', text('dev'), coerceFalse); |
|
}; |
|
const addCypressOpenCommand = program => { |
|
return program.command('open').usage('[options]').description('Opens Cypress in the interactive GUI.').option('-b, --browser <browser-path>', text('browser')).option('--component', text('component')).option('-c, --config <config>', text('config')).option('-C, --config-file <config-file>', text('configFile')).option('-d, --detached [bool]', text('detached'), coerceFalse).option('--e2e', text('e2e')).option('-e, --env <env>', text('env')).option('--global', text('global')).option('-p, --port <port>', text('port')).option('-P, --project <project-path>', text('project')).option('--dev', text('dev'), coerceFalse); |
|
}; |
|
const maybeAddInspectFlags = program => { |
|
if (process.argv.includes('--dev')) { |
|
return program.option('--inspect', 'Node option').option('--inspect-brk', 'Node option'); |
|
} |
|
return program; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const castCypressOptions = opts => { |
|
|
|
|
|
|
|
const castOpts = { |
|
...opts |
|
}; |
|
if (_.has(opts, 'port')) { |
|
castOpts.port = coerceAnyStringToInt(opts.port); |
|
} |
|
return castOpts; |
|
}; |
|
module.exports = { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseRunCommand(args) { |
|
return new Promise((resolve, reject) => { |
|
if (!Array.isArray(args)) { |
|
return reject(new Error('Expected array of arguments')); |
|
} |
|
|
|
|
|
|
|
|
|
const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args]; |
|
cliArgs.unshift(null, null); |
|
debug('creating program parser'); |
|
const program = createProgram(); |
|
maybeAddInspectFlags(addCypressRunCommand(program)).action((...fnArgs) => { |
|
debug('parsed Cypress run %o', fnArgs); |
|
const options = parseVariableOpts(fnArgs, cliArgs); |
|
debug('parsed options %o', options); |
|
const casted = castCypressOptions(options); |
|
debug('casted options %o', casted); |
|
resolve(casted); |
|
}); |
|
debug('parsing args: %o', cliArgs); |
|
program.parse(cliArgs); |
|
}); |
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseOpenCommand(args) { |
|
return new Promise((resolve, reject) => { |
|
if (!Array.isArray(args)) { |
|
return reject(new Error('Expected array of arguments')); |
|
} |
|
|
|
|
|
|
|
|
|
const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args]; |
|
cliArgs.unshift(null, null); |
|
debug('creating program parser'); |
|
const program = createProgram(); |
|
maybeAddInspectFlags(addCypressOpenCommand(program)).action((...fnArgs) => { |
|
debug('parsed Cypress open %o', fnArgs); |
|
const options = parseVariableOpts(fnArgs, cliArgs); |
|
debug('parsed options %o', options); |
|
const casted = castCypressOptions(options); |
|
debug('casted options %o', casted); |
|
resolve(casted); |
|
}); |
|
debug('parsing args: %o', cliArgs); |
|
program.parse(cliArgs); |
|
}); |
|
}, |
|
|
|
|
|
|
|
init(args) { |
|
if (!args) { |
|
args = process.argv; |
|
} |
|
const { |
|
CYPRESS_INTERNAL_ENV, |
|
CYPRESS_DOWNLOAD_USE_CA |
|
} = process.env; |
|
if (process.env.CYPRESS_DOWNLOAD_USE_CA) { |
|
let msg = ` |
|
${logSymbols.warning} Warning: It looks like you're setting CYPRESS_DOWNLOAD_USE_CA=${CYPRESS_DOWNLOAD_USE_CA} |
|
|
|
The environment variable "CYPRESS_DOWNLOAD_USE_CA" is no longer required to be set. |
|
|
|
You can safely unset this environment variable. |
|
`; |
|
logger.log(); |
|
logger.warn(stripIndent(msg)); |
|
logger.log(); |
|
} |
|
if (!util.isValidCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) { |
|
debug('invalid CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV); |
|
return errors.exitWithError(errors.errors.invalidCypressEnv)(`CYPRESS_INTERNAL_ENV=${CYPRESS_INTERNAL_ENV}`); |
|
} |
|
if (util.isNonProductionCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) { |
|
debug('non-production CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV); |
|
let msg = ` |
|
${logSymbols.warning} Warning: It looks like you're passing CYPRESS_INTERNAL_ENV=${CYPRESS_INTERNAL_ENV} |
|
|
|
The environment variable "CYPRESS_INTERNAL_ENV" is reserved and should only be used internally. |
|
|
|
Unset the "CYPRESS_INTERNAL_ENV" environment variable and run Cypress again. |
|
`; |
|
logger.log(); |
|
logger.warn(stripIndent(msg)); |
|
logger.log(); |
|
} |
|
const program = createProgram(); |
|
program.command('help').description('Shows CLI help and exits').action(() => { |
|
program.help(); |
|
}); |
|
const handleVersion = cmd => { |
|
return cmd.option('--component <package|binary|electron|node>', 'component to report version for').action((opts, ...other) => { |
|
showVersions(util.parseOpts(opts)); |
|
}); |
|
}; |
|
handleVersion(program.storeOptionsAsProperties().option('-v, --version', text('version')).command('version').description(text('version'))); |
|
maybeAddInspectFlags(addCypressOpenCommand(program)).action(opts => { |
|
debug('opening Cypress'); |
|
require('./exec/open').start(util.parseOpts(opts)).then(util.exit).catch(util.logErrorExit1); |
|
}); |
|
maybeAddInspectFlags(addCypressRunCommand(program)).action((...fnArgs) => { |
|
debug('running Cypress with args %o', fnArgs); |
|
require('./exec/run').start(parseVariableOpts(fnArgs, args)).then(util.exit).catch(util.logErrorExit1); |
|
}); |
|
program.command('open-ct').usage('[options]').description('Opens Cypress component testing interactive mode. Deprecated: use "open --component"').option('-b, --browser <browser-path>', text('browser')).option('-c, --config <config>', text('config')).option('-C, --config-file <config-file>', text('configFile')).option('-d, --detached [bool]', text('detached'), coerceFalse).option('-e, --env <env>', text('env')).option('--global', text('global')).option('-p, --port <port>', text('port')).option('-P, --project <project-path>', text('project')).option('--dev', text('dev'), coerceFalse).action(opts => { |
|
debug('opening Cypress'); |
|
const msg = ` |
|
${logSymbols.warning} Warning: open-ct is deprecated and will be removed in a future release. |
|
|
|
Use \`cypress open --component\` instead. |
|
`; |
|
logger.warn(); |
|
logger.warn(stripIndent(msg)); |
|
logger.warn(); |
|
require('./exec/open').start({ |
|
...util.parseOpts(opts), |
|
testingType: 'component' |
|
}).then(util.exit).catch(util.logErrorExit1); |
|
}); |
|
program.command('run-ct').usage('[options]').description('Runs all Cypress component testing suites. Deprecated: use "run --component"').option('-b, --browser <browser-name-or-path>', text('browser')).option('--ci-build-id <id>', text('ciBuildId')).option('-c, --config <config>', text('config')).option('-C, --config-file <config-file>', text('configFile')).option('-e, --env <env>', text('env')).option('--group <name>', text('group')).option('-k, --key <record-key>', text('key')).option('--headed', text('headed')).option('--headless', text('headless')).option('--no-exit', text('exit')).option('--parallel', text('parallel')).option('-p, --port <port>', text('port')).option('-P, --project <project-path>', text('project')).option('-q, --quiet', text('quiet')).option('--record [bool]', text('record'), coerceFalse).option('-r, --reporter <reporter>', text('reporter')).option('-o, --reporter-options <reporter-options>', text('reporterOptions')).option('-s, --spec <spec>', text('spec')).option('-t, --tag <tag>', text('tag')).option('--dev', text('dev'), coerceFalse).action(opts => { |
|
debug('running Cypress run-ct'); |
|
const msg = ` |
|
${logSymbols.warning} Warning: run-ct is deprecated and will be removed in a future release. |
|
Use \`cypress run --component\` instead. |
|
`; |
|
logger.warn(); |
|
logger.warn(stripIndent(msg)); |
|
logger.warn(); |
|
require('./exec/run').start({ |
|
...util.parseOpts(opts), |
|
testingType: 'component' |
|
}).then(util.exit).catch(util.logErrorExit1); |
|
}); |
|
program.command('install').usage('[options]').description('Installs the Cypress executable matching this package\'s version').option('-f, --force', text('forceInstall')).action(opts => { |
|
require('./tasks/install').start(util.parseOpts(opts)).catch(util.logErrorExit1); |
|
}); |
|
program.command('verify').usage('[options]').description('Verifies that Cypress is installed correctly and executable').option('--dev', text('dev'), coerceFalse).action(opts => { |
|
const defaultOpts = { |
|
force: true, |
|
welcomeMessage: false |
|
}; |
|
const parsedOpts = util.parseOpts(opts); |
|
const options = _.extend(parsedOpts, defaultOpts); |
|
require('./tasks/verify').start(options).catch(util.logErrorExit1); |
|
}); |
|
program.command('cache').usage('[command]').description('Manages the Cypress binary cache').option('list', text('cacheList')).option('path', text('cachePath')).option('clear', text('cacheClear')).option('prune', text('cachePrune')).option('--size', text('cacheSize')).action(function (opts, args) { |
|
if (!args || !args.length) { |
|
this.outputHelp(); |
|
util.exit(1); |
|
} |
|
const [command] = args; |
|
if (!_.includes(['list', 'path', 'clear', 'prune'], command)) { |
|
unknownOption.call(this, `cache ${command}`, 'command'); |
|
} |
|
if (command === 'list') { |
|
debug('cache command %o', { |
|
command, |
|
size: opts.size |
|
}); |
|
return cache.list(opts.size).catch({ |
|
code: 'ENOENT' |
|
}, () => { |
|
logger.always('No cached binary versions were found.'); |
|
process.exit(0); |
|
}).catch(e => { |
|
debug('cache list command failed with "%s"', e.message); |
|
util.logErrorExit1(e); |
|
}); |
|
} |
|
cache[command](); |
|
}); |
|
program.command('info').usage('[command]').description('Prints Cypress and system information').option('--dev', text('dev'), coerceFalse).action(opts => { |
|
require('./exec/info').start(opts).then(util.exit).catch(util.logErrorExit1); |
|
}); |
|
debug('cli starts with arguments %j', args); |
|
util.printNodeOptions(); |
|
|
|
|
|
if (args.length <= 2) { |
|
debug('printing help'); |
|
program.help(); |
|
|
|
} |
|
|
|
const firstCommand = args[2]; |
|
if (!_.includes(knownCommands, firstCommand)) { |
|
debug('unknown command %s', firstCommand); |
|
logger.error('Unknown command', `"${firstCommand}"`); |
|
program.outputHelp(); |
|
return util.exit(1); |
|
} |
|
if (includesVersion(args)) { |
|
|
|
|
|
|
|
|
|
handleVersion(program); |
|
} |
|
debug('program parsing arguments'); |
|
return program.parse(args); |
|
} |
|
}; |
|
|
|
|
|
if (!module.parent) { |
|
logger.error('This CLI module should be required from another Node module'); |
|
logger.error('and not executed directly'); |
|
process.exit(-1); |
|
} |