cli.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. 'use strict';
  2. var _ = require('lodash');
  3. var commander = require('commander');
  4. var _require = require('common-tags'),
  5. stripIndent = _require.stripIndent;
  6. var logSymbols = require('log-symbols');
  7. var debug = require('debug')('cypress:cli:cli');
  8. var util = require('./util');
  9. var logger = require('./logger');
  10. var errors = require('./errors');
  11. var cache = require('./tasks/cache');
  12. // patch "commander" method called when a user passed an unknown option
  13. // we want to print help for the current command and exit with an error
  14. function unknownOption(flag) {
  15. var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'option';
  16. if (this._allowUnknownOption) return;
  17. logger.error();
  18. logger.error(' error: unknown ' + type + ':', flag);
  19. logger.error();
  20. this.outputHelp();
  21. util.exit(1);
  22. }
  23. commander.Command.prototype.unknownOption = unknownOption;
  24. var coerceFalse = function coerceFalse(arg) {
  25. return arg !== 'false';
  26. };
  27. var spaceDelimitedArgsMsg = function spaceDelimitedArgsMsg(flag, args) {
  28. var msg = '\n ' + logSymbols.warning + ' Warning: It looks like you\'re passing --' + flag + ' a space-separated list of arguments:\n\n "' + args.join(' ') + '"\n\n This will work, but it\'s not recommended.\n\n If you are trying to pass multiple arguments, separate them with commas instead:\n cypress run --' + flag + ' arg1,arg2,arg3\n ';
  29. if (flag === 'spec') {
  30. msg += '\n The most common cause of this warning is using an unescaped glob pattern. If you are\n trying to pass a glob pattern, escape it using quotes:\n cypress run --spec "**/*.spec.js"\n ';
  31. }
  32. logger.log();
  33. logger.warn(stripIndent(msg));
  34. logger.log();
  35. };
  36. var parseVariableOpts = function parseVariableOpts(fnArgs, args) {
  37. var opts = fnArgs.pop();
  38. if (fnArgs.length && (opts.spec || opts.tag)) {
  39. // this will capture space-delimited args after
  40. // flags that could have possible multiple args
  41. // but before the next option
  42. // --spec spec1 spec2 or --tag foo bar
  43. var multiArgFlags = _.compact([opts.spec ? 'spec' : opts.spec, opts.tag ? 'tag' : opts.tag]);
  44. _.forEach(multiArgFlags, function (flag) {
  45. var argIndex = _.indexOf(args, '--' + flag) + 2;
  46. var nextOptOffset = _.findIndex(_.slice(args, argIndex), function (arg) {
  47. return _.startsWith(arg, '--');
  48. });
  49. var endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length;
  50. var maybeArgs = _.slice(args, argIndex, endIndex);
  51. var extraArgs = _.intersection(maybeArgs, fnArgs);
  52. if (extraArgs.length) {
  53. opts[flag] = [opts[flag]].concat(extraArgs);
  54. spaceDelimitedArgsMsg(flag, opts[flag]);
  55. opts[flag] = opts[flag].join(',');
  56. }
  57. });
  58. }
  59. debug('variable-length opts parsed %o', { args: args, opts: opts });
  60. return util.parseOpts(opts);
  61. };
  62. var descriptions = {
  63. browserOpenMode: 'path to a custom browser to be added to the list of available browsers in Cypress',
  64. browserRunMode: '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.',
  65. cacheClear: 'delete all cached binaries',
  66. cacheList: 'list cached binary versions',
  67. cachePath: 'print the path to the binary cache',
  68. 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',
  69. config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.',
  70. configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.',
  71. detached: 'runs Cypress application in detached mode',
  72. dev: 'runs cypress in development and bypasses binary check',
  73. env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json',
  74. exit: 'keep the browser open after tests finish',
  75. forceInstall: 'force install the Cypress binary',
  76. global: 'force Cypress into global mode as if its globally installed',
  77. group: 'a named group for recorded runs in the Cypress Dashboard',
  78. headed: 'displays the browser instead of running headlessly (defaults to true for Chrome-family browsers)',
  79. headless: 'hide the browser instead of running headed (defaults to true for Electron)',
  80. key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
  81. parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
  82. port: 'runs Cypress on a specific port. overrides any value in cypress.json.',
  83. project: 'path to the project',
  84. record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
  85. reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"',
  86. reporterOptions: 'options for the mocha reporter. defaults to "null"',
  87. spec: 'runs specific spec file(s). defaults to "all"',
  88. tag: 'named tag(s) for recorded runs in the Cypress Dashboard',
  89. version: 'prints Cypress version'
  90. };
  91. var knownCommands = ['cache', 'help', '-h', '--help', 'install', 'open', 'run', 'verify', '-v', '--version', 'version'];
  92. var text = function text(description) {
  93. if (!descriptions[description]) {
  94. throw new Error('Could not find description for: ' + description);
  95. }
  96. return descriptions[description];
  97. };
  98. function includesVersion(args) {
  99. return _.includes(args, 'version') || _.includes(args, '--version') || _.includes(args, '-v');
  100. }
  101. function showVersions() {
  102. debug('printing Cypress version');
  103. return require('./exec/versions').getVersions().then(function () {
  104. var versions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  105. logger.log('Cypress package version:', versions.package);
  106. logger.log('Cypress binary version:', versions.binary);
  107. process.exit(0);
  108. }).catch(util.logErrorExit1);
  109. }
  110. module.exports = {
  111. init: function init(args) {
  112. if (!args) {
  113. args = process.argv;
  114. }
  115. if (!util.isValidCypressEnvValue(process.env.CYPRESS_ENV)) {
  116. debug('invalid CYPRESS_ENV value', process.env.CYPRESS_ENV);
  117. return errors.exitWithError(errors.errors.invalidCypressEnv)('CYPRESS_ENV=' + process.env.CYPRESS_ENV);
  118. }
  119. var program = new commander.Command();
  120. // bug in commander not printing name
  121. // in usage help docs
  122. program._name = 'cypress';
  123. program.usage('<command> [options]');
  124. program.command('help').description('Shows CLI help and exits').action(function () {
  125. program.help();
  126. });
  127. program.option('-v, --version', text('version')).command('version').description(text('version')).action(showVersions);
  128. program.command('run').usage('[options]').description('Runs Cypress tests from the CLI without the GUI').option('-b, --browser <browser-name-or-path>', text('browserRunMode')).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('--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(function () {
  129. for (var _len = arguments.length, fnArgs = Array(_len), _key = 0; _key < _len; _key++) {
  130. fnArgs[_key] = arguments[_key];
  131. }
  132. debug('running Cypress with args %o', fnArgs);
  133. require('./exec/run').start(parseVariableOpts(fnArgs, args)).then(util.exit).catch(util.logErrorExit1);
  134. });
  135. program.command('open').usage('[options]').description('Opens Cypress in the interactive GUI.').option('-b, --browser <browser-path>', text('browserOpenMode')).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(function (opts) {
  136. debug('opening Cypress');
  137. require('./exec/open').start(util.parseOpts(opts)).catch(util.logErrorExit1);
  138. });
  139. program.command('install').usage('[options]').description('Installs the Cypress executable matching this package\'s version').option('-f, --force', text('forceInstall')).action(function (opts) {
  140. require('./tasks/install').start(util.parseOpts(opts)).catch(util.logErrorExit1);
  141. });
  142. program.command('verify').usage('[options]').description('Verifies that Cypress is installed correctly and executable').option('--dev', text('dev'), coerceFalse).action(function (opts) {
  143. var defaultOpts = { force: true, welcomeMessage: false };
  144. var parsedOpts = util.parseOpts(opts);
  145. var options = _.extend(parsedOpts, defaultOpts);
  146. require('./tasks/verify').start(options).catch(util.logErrorExit1);
  147. });
  148. program.command('cache').usage('[command]').description('Manages the Cypress binary cache').option('list', text('cacheList')).option('path', text('cachePath')).option('clear', text('cacheClear')).action(function (opts) {
  149. if (!_.isString(opts)) {
  150. this.outputHelp();
  151. util.exit(1);
  152. }
  153. if (opts.command || !_.includes(['list', 'path', 'clear'], opts)) {
  154. unknownOption.call(this, 'cache ' + opts, 'command');
  155. }
  156. cache[opts]();
  157. });
  158. debug('cli starts with arguments %j', args);
  159. util.printNodeOptions();
  160. // if there are no arguments
  161. if (args.length <= 2) {
  162. debug('printing help');
  163. program.help();
  164. // exits
  165. }
  166. var firstCommand = args[2];
  167. if (!_.includes(knownCommands, firstCommand)) {
  168. debug('unknown command %s', firstCommand);
  169. logger.error('Unknown command', '"' + firstCommand + '"');
  170. program.outputHelp();
  171. return util.exit(1);
  172. }
  173. if (includesVersion(args)) {
  174. // commander 2.11.0 changes behavior
  175. // and now does not understand top level options
  176. // .option('-v, --version').command('version')
  177. // so we have to manually catch '-v, --version'
  178. return showVersions();
  179. }
  180. debug('program parsing arguments');
  181. return program.parse(args);
  182. }
  183. };
  184. if (!module.parent) {
  185. logger.error('This CLI module should be required from another Node module');
  186. logger.error('and not executed directly');
  187. process.exit(-1);
  188. }