mirror of
https://github.com/cypress-io/cypress.git
synced 2026-03-15 05:40:29 -05:00
* cli, server: introduce --config-file argument * server: remove unused import * server: wip * server: consider --config-file in settings * server: pass options to settings.read from config * server: store options in Project class, pass to all settings calls * server: _initPlugins needs to accept options, for being called from server * server: accept optional external options in open * cli: update help snapshots * server: realizing now that these were written like this so they could be stubbed - removing some unnecessary usages of @options * cli: pass configFile when it's false * server: --config-file false and --config-file blah.json work * server: add unit tests for --config-file * server: pass configFile to desktop-gui * desktop-gui: display 'cypress.json' according to --config-file arg * desktop-gui: add integration tests for --config-file * cli: add tests for --config-file * PR changes * PR changes * cli: update snapshots * server: updating error messages * runner: update cypress.json mention * fixing name overlap * server: integration tests for --config-file * runner: update Header component tests * cli: fix snapshot * desktop-gui: fix test * driver: fixing error messages - not really any visibility to cli args from here so just static strings * server: update snapshots * server: update snapshots * cli: updating snapshot * driver: how did i miss this? * add skipped blank line to the snapshot * fix missing proxy require statement (was lost in merge of develop)...weird * add module API defs to types * module API tests * send better error when config file can't be found * fix dtslint test * update cli help to use 'configuration file' * update snapshot using 7.7.1 in place * fix failing config_spec * be.visible * show custom config file name in driver errors * add tests for non-default config file in driver error messages * single-quote config file name * 🙅 IIFEs 🙅 * 🤦 * fix failing test * fix failing test, cleanup * lint * delete duplicate coffee spec * Update run.js * Delete app_spec.js.mp4 * in open mode, only store projects to recents list if 'cypress.json' is the configFile discussion: https://git.io/JeGyF * feedback
368 lines
11 KiB
JavaScript
368 lines
11 KiB
JavaScript
const _ = require('lodash')
|
|
const commander = require('commander')
|
|
const { stripIndent } = require('common-tags')
|
|
const logSymbols = require('log-symbols')
|
|
const debug = require('debug')('cypress:cli')
|
|
const util = require('./util')
|
|
const logger = require('./logger')
|
|
const errors = require('./errors')
|
|
const cache = require('./tasks/cache')
|
|
|
|
// patch "commander" method called when a user passed an unknown option
|
|
// we want to print help for the current command and exit with an error
|
|
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 spaceDelimitedSpecsMsg = (files) => {
|
|
logger.log()
|
|
logger.warn(stripIndent`
|
|
${
|
|
logSymbols.warning
|
|
} Warning: It looks like you're passing --spec a space-separated list of files:
|
|
|
|
"${files.join(' ')}"
|
|
|
|
This will work, but it's not recommended.
|
|
|
|
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"
|
|
|
|
If you are trying to pass multiple spec filenames, separate them by commas instead:
|
|
cypress run --spec spec1,spec2,spec3
|
|
`)
|
|
|
|
logger.log()
|
|
}
|
|
|
|
const parseVariableOpts = (fnArgs, args) => {
|
|
const opts = fnArgs.pop()
|
|
|
|
if (fnArgs.length && opts.spec) {
|
|
// this will capture space-delimited specs after --spec spec1 but before the next option
|
|
|
|
const argIndex = _.indexOf(args, '--spec') + 2
|
|
const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => {
|
|
return _.startsWith(arg, '--')
|
|
})
|
|
const endIndex =
|
|
nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length
|
|
|
|
const maybeSpecs = _.slice(args, argIndex, endIndex)
|
|
const extraSpecs = _.intersection(maybeSpecs, fnArgs)
|
|
|
|
if (extraSpecs.length) {
|
|
opts.spec = [opts.spec].concat(extraSpecs)
|
|
spaceDelimitedSpecsMsg(opts.spec)
|
|
opts.spec = opts.spec.join(',')
|
|
}
|
|
}
|
|
|
|
return parseOpts(opts)
|
|
}
|
|
|
|
const parseOpts = (opts) => {
|
|
opts = _.pick(
|
|
opts,
|
|
'project',
|
|
'spec',
|
|
'reporter',
|
|
'reporterOptions',
|
|
'path',
|
|
'destination',
|
|
'port',
|
|
'env',
|
|
'cypressVersion',
|
|
'config',
|
|
'record',
|
|
'key',
|
|
'configFile',
|
|
'browser',
|
|
'detached',
|
|
'headed',
|
|
'global',
|
|
'dev',
|
|
'force',
|
|
'exit',
|
|
'cachePath',
|
|
'cacheList',
|
|
'cacheClear',
|
|
'parallel',
|
|
'group',
|
|
'ciBuildId'
|
|
)
|
|
|
|
if (opts.exit) {
|
|
opts = _.omit(opts, 'exit')
|
|
}
|
|
|
|
debug('parsed cli options', opts)
|
|
|
|
return opts
|
|
}
|
|
|
|
const descriptions = {
|
|
record:
|
|
'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
|
|
key:
|
|
'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
|
|
spec: 'runs a specific spec file. defaults to "all"',
|
|
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"',
|
|
port: 'runs Cypress on a specific port. overrides any value in the configuration file.',
|
|
env: 'sets environment variables. separate multiple values with a comma. overrides any value in the configuration file or cypress.env.json',
|
|
config: 'sets configuration values. separate multiple values with a comma. overrides any value in the configuration file.',
|
|
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.',
|
|
browserOpenMode: 'path to a custom browser to be added to the list of available browsers in Cypress',
|
|
detached: 'runs Cypress application in detached mode',
|
|
project: 'path to the project',
|
|
global: 'force Cypress into global mode as if its globally installed',
|
|
configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.',
|
|
version: 'prints Cypress version',
|
|
headed: 'displays the Electron browser instead of running headlessly',
|
|
dev: 'runs cypress in development and bypasses binary check',
|
|
forceInstall: 'force install the Cypress binary',
|
|
exit: 'keep the browser open after tests finish',
|
|
cachePath: 'print the path to the binary cache',
|
|
cacheList: 'list cached binary versions',
|
|
cacheClear: 'delete all cached binaries',
|
|
group: 'a named group for recorded runs in the Cypress dashboard',
|
|
parallel:
|
|
'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
|
|
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',
|
|
}
|
|
|
|
const knownCommands = [
|
|
'version',
|
|
'run',
|
|
'open',
|
|
'install',
|
|
'verify',
|
|
'-v',
|
|
'--version',
|
|
'help',
|
|
'-h',
|
|
'--help',
|
|
'cache',
|
|
]
|
|
|
|
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, '--version') ||
|
|
_.includes(args, '-v')
|
|
)
|
|
}
|
|
|
|
function showVersions () {
|
|
debug('printing Cypress version')
|
|
|
|
return require('./exec/versions')
|
|
.getVersions()
|
|
.then((versions = {}) => {
|
|
logger.log('Cypress package version:', versions.package)
|
|
logger.log('Cypress binary version:', versions.binary)
|
|
process.exit(0)
|
|
})
|
|
.catch(util.logErrorExit1)
|
|
}
|
|
|
|
module.exports = {
|
|
init (args) {
|
|
if (!args) {
|
|
args = process.argv
|
|
}
|
|
|
|
if (!util.isValidCypressEnvValue(process.env.CYPRESS_ENV)) {
|
|
debug('invalid CYPRESS_ENV value', process.env.CYPRESS_ENV)
|
|
|
|
return errors.exitWithError(errors.errors.invalidCypressEnv)(
|
|
`CYPRESS_ENV=${process.env.CYPRESS_ENV}`
|
|
)
|
|
}
|
|
|
|
const program = new commander.Command()
|
|
|
|
// bug in commaner not printing name
|
|
// in usage help docs
|
|
program._name = 'cypress'
|
|
|
|
program.usage('<command> [options]')
|
|
|
|
program
|
|
.command('help')
|
|
.description('Shows CLI help and exits')
|
|
.action(() => {
|
|
program.help()
|
|
})
|
|
|
|
program
|
|
.option('-v, --version', text('version'))
|
|
.command('version')
|
|
.description(text('version'))
|
|
.action(showVersions)
|
|
|
|
program
|
|
.command('run')
|
|
.usage('[options]')
|
|
.description('Runs Cypress tests from the CLI without the GUI')
|
|
.option('--record [bool]', text('record'), coerceFalse)
|
|
.option('--headed', text('headed'))
|
|
.option('-k, --key <record-key>', text('key'))
|
|
.option('-s, --spec <spec>', text('spec'))
|
|
.option('-r, --reporter <reporter>', text('reporter'))
|
|
.option(
|
|
'-o, --reporter-options <reporter-options>',
|
|
text('reporterOptions')
|
|
)
|
|
.option('-p, --port <port>', text('port'))
|
|
.option('-e, --env <env>', text('env'))
|
|
.option('-c, --config <config>', text('config'))
|
|
.option('-C, --config-file <config-file>', text('configFile'))
|
|
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
|
|
.option('-P, --project <project-path>', text('project'))
|
|
.option('--parallel', text('parallel'))
|
|
.option('--group <name>', text('group'))
|
|
.option('--ci-build-id <id>', text('ciBuildId'))
|
|
.option('--no-exit', text('exit'))
|
|
.option('--dev', text('dev'), coerceFalse)
|
|
.action((...fnArgs) => {
|
|
debug('running Cypress')
|
|
require('./exec/run')
|
|
.start(parseVariableOpts(fnArgs, args))
|
|
.then(util.exit)
|
|
.catch(util.logErrorExit1)
|
|
})
|
|
|
|
program
|
|
.command('open')
|
|
.usage('[options]')
|
|
.description('Opens Cypress in the interactive GUI.')
|
|
.option('-p, --port <port>', text('port'))
|
|
.option('-e, --env <env>', text('env'))
|
|
.option('-c, --config <config>', text('config'))
|
|
.option('-C, --config-file <config-file>', text('configFile'))
|
|
.option('-d, --detached [bool]', text('detached'), coerceFalse)
|
|
.option('-b, --browser <browser-path>', text('browserOpenMode'))
|
|
.option('-P, --project <project-path>', text('project'))
|
|
.option('--global', text('global'))
|
|
.option('--dev', text('dev'), coerceFalse)
|
|
.action((opts) => {
|
|
debug('opening Cypress')
|
|
require('./exec/open')
|
|
.start(parseOpts(opts))
|
|
.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(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 = 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'))
|
|
.action(function (opts) {
|
|
if (!_.isString(opts)) {
|
|
this.outputHelp()
|
|
util.exit(1)
|
|
}
|
|
|
|
if (opts.command || !_.includes(['list', 'path', 'clear'], opts)) {
|
|
unknownOption.call(this, `cache ${opts}`, 'command')
|
|
}
|
|
|
|
cache[opts]()
|
|
})
|
|
|
|
debug('cli starts with arguments %j', args)
|
|
util.printNodeOptions()
|
|
|
|
// if there are no arguments
|
|
if (args.length <= 2) {
|
|
debug('printing help')
|
|
program.help()
|
|
// exits
|
|
}
|
|
|
|
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)) {
|
|
// commander 2.11.0 changes behavior
|
|
// and now does not understand top level options
|
|
// .option('-v, --version').command('version')
|
|
// so we have to manually catch '-v, --version'
|
|
return showVersions()
|
|
}
|
|
|
|
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)
|
|
}
|