mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-26 10:59:23 -06:00
cli: expose method that parses cypress run CLI logic (#7798)
Co-authored-by: Zach Bloomquist <github@chary.us>
This commit is contained in:
29
circle.yml
29
circle.yml
@@ -1325,14 +1325,24 @@ jobs:
|
||||
repo: cypress-example-recipes
|
||||
command: npm run test:ci:firefox
|
||||
|
||||
# This is a special job. It allows you to test the current
|
||||
# built test runner against a pull request in the repo
|
||||
# cypress-example-recipes.
|
||||
# Imagine you are working on a feature and want to show / test a recipe
|
||||
# You would need to run the built test runner before release
|
||||
# against a PR that cannot be merged until the new version
|
||||
# of the test runner is released.
|
||||
# Use:
|
||||
# specify pull request number
|
||||
# and the recipe folder
|
||||
test-binary-against-recipe-pull-request:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- test-binary-against-repo:
|
||||
repo: cypress-example-recipes
|
||||
command: npm run test:ci
|
||||
pull_request_id: 503
|
||||
folder: examples/stubbing-spying__window-fetch
|
||||
command: npm test
|
||||
pull_request_id: 513
|
||||
folder: examples/fundamentals__module-api-wrap
|
||||
|
||||
"test-binary-against-kitchensink":
|
||||
<<: *defaults
|
||||
@@ -1652,12 +1662,21 @@ linux-workflow: &linux-workflow
|
||||
requires:
|
||||
- build-binary
|
||||
- build-npm-package
|
||||
# when working on a feature or a fix,
|
||||
# you are probably working in a branch
|
||||
# and you want to run a specific PR in the cypress-example-recipes
|
||||
# against this branch. This workflow job includes
|
||||
# the job but only when it runs on specific branch
|
||||
# DO NOT DELETE THIS JOB BEFORE MERGING TO DEVELOP
|
||||
# on "develop" this branch will be ignored anyway
|
||||
# and someone else might use this job definition for another
|
||||
# feature branch and would just update the branch filter
|
||||
- test-binary-against-recipe-pull-request:
|
||||
name: Test fetch polyfill
|
||||
name: Test cypress run parsing
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- polyfill-fetch
|
||||
- cli-to-module-api-7760
|
||||
requires:
|
||||
- build-binary
|
||||
- build-npm-package
|
||||
|
||||
140
cli/lib/cli.js
140
cli/lib/cli.js
@@ -1,4 +1,5 @@
|
||||
const _ = require('lodash')
|
||||
const R = require('ramda')
|
||||
const commander = require('commander')
|
||||
const { stripIndent } = require('common-tags')
|
||||
const logSymbols = require('log-symbols')
|
||||
@@ -25,6 +26,10 @@ 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:
|
||||
@@ -162,7 +167,109 @@ function showVersions () {
|
||||
.catch(util.logErrorExit1)
|
||||
}
|
||||
|
||||
const createProgram = () => {
|
||||
const program = new commander.Command()
|
||||
|
||||
// bug in commander not printing name
|
||||
// in usage help docs
|
||||
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('-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('-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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts known command line options for "cypress run" to their intended type.
|
||||
* For example if the user passes "--port 5005" the ".port" property should be
|
||||
* a number 5005 and not a string "5005".
|
||||
*
|
||||
* Returns a clone of the original object.
|
||||
*/
|
||||
const castCypressRunOptions = (opts) => {
|
||||
// only properties that have type "string | false" in our TS definition
|
||||
// require special handling, because CLI parsing takes care of purely
|
||||
// boolean arguments
|
||||
const result = R.evolve({
|
||||
port: coerceAnyStringToInt,
|
||||
configFile: coerceFalse,
|
||||
})(opts)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Parses `cypress run` command line option array into an object
|
||||
* with options that you can feed into a `cypress.run()` module API call.
|
||||
* @example
|
||||
* const options = parseRunCommand(['cypress', 'run', '--browser', 'chrome'])
|
||||
* // options is {browser: 'chrome'}
|
||||
*/
|
||||
parseRunCommand (args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!Array.isArray(args)) {
|
||||
return reject(new Error('Expected array of arguments'))
|
||||
}
|
||||
|
||||
// make a copy of the input arguments array
|
||||
// and add placeholders where "node ..." would usually be
|
||||
// also remove "cypress" keyword at the start if present
|
||||
const cliArgs = args[0] === 'cypress' ? [...args.slice(1)] : [...args]
|
||||
|
||||
cliArgs.unshift(null, null)
|
||||
|
||||
debug('creating program parser')
|
||||
const program = createProgram()
|
||||
|
||||
addCypressRunCommand(program)
|
||||
.action((...fnArgs) => {
|
||||
debug('parsed Cypress run %o', fnArgs)
|
||||
const options = parseVariableOpts(fnArgs, cliArgs)
|
||||
|
||||
debug('parsed options %o', options)
|
||||
|
||||
const casted = castCypressRunOptions(options)
|
||||
|
||||
debug('casted options %o', casted)
|
||||
resolve(casted)
|
||||
})
|
||||
|
||||
debug('parsing args: %o', cliArgs)
|
||||
program.parse(cliArgs)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Parses the command line and kicks off Cypress process.
|
||||
*/
|
||||
init (args) {
|
||||
if (!args) {
|
||||
args = process.argv
|
||||
@@ -194,13 +301,7 @@ module.exports = {
|
||||
logger.log()
|
||||
}
|
||||
|
||||
const program = new commander.Command()
|
||||
|
||||
// bug in commander not printing name
|
||||
// in usage help docs
|
||||
program._name = 'cypress'
|
||||
|
||||
program.usage('<command> [options]')
|
||||
const program = createProgram()
|
||||
|
||||
program
|
||||
.command('help')
|
||||
@@ -215,30 +316,7 @@ module.exports = {
|
||||
.description(text('version'))
|
||||
.action(showVersions)
|
||||
|
||||
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('-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)
|
||||
addCypressRunCommand(program)
|
||||
.action((...fnArgs) => {
|
||||
debug('running Cypress with args %o', fnArgs)
|
||||
require('./exec/run')
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require('./fs')
|
||||
const open = require('./exec/open')
|
||||
const run = require('./exec/run')
|
||||
const util = require('./util')
|
||||
const cli = require('./cli')
|
||||
|
||||
const cypressModuleApi = {
|
||||
/**
|
||||
@@ -50,6 +51,22 @@ const cypressModuleApi = {
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
cli: {
|
||||
/**
|
||||
* Parses CLI arguments into an object that you can pass to "cypress.run"
|
||||
* @example
|
||||
* const cypress = require('cypress')
|
||||
* const cli = ['cypress', 'run', '--browser', 'firefox']
|
||||
* const options = await cypress.cli.parseRunArguments(cli)
|
||||
* // options is {browser: 'firefox'}
|
||||
* await cypress.run(options)
|
||||
* @see https://on.cypress.io/module-api
|
||||
*/
|
||||
parseRunArguments (args) {
|
||||
return cli.parseRunCommand(args)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = cypressModuleApi
|
||||
|
||||
@@ -172,4 +172,71 @@ describe('cypress', function () {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('cli', function () {
|
||||
describe('.parseRunArguments', function () {
|
||||
it('parses CLI cypress run arguments', async () => {
|
||||
const args = 'cypress run --browser chrome --spec my/test/spec.js'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
expect(options).to.deep.equal({
|
||||
browser: 'chrome',
|
||||
spec: 'my/test/spec.js',
|
||||
})
|
||||
})
|
||||
|
||||
it('parses CLI cypress run shorthand arguments', async () => {
|
||||
const args = 'cypress run -b firefox -p 5005 --headed --quiet'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
expect(options).to.deep.equal({
|
||||
browser: 'firefox',
|
||||
port: 5005,
|
||||
headed: true,
|
||||
quiet: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('coerces --record and --dev', async () => {
|
||||
const args = 'cypress run --record false --dev true'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
expect(options).to.deep.equal({
|
||||
record: false,
|
||||
dev: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses config file false', async () => {
|
||||
const args = 'cypress run --config-file false'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
expect(options).to.deep.equal({
|
||||
configFile: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses config', async () => {
|
||||
const args = 'cypress run --config baseUrl=localhost,video=true'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
// we don't need to convert the config into an object
|
||||
// since the logic inside cypress.run handles that
|
||||
expect(options).to.deep.equal({
|
||||
config: 'baseUrl=localhost,video=true',
|
||||
})
|
||||
})
|
||||
|
||||
it('parses env', async () => {
|
||||
const args = 'cypress run --env MY_NUMBER=42,MY_FLAG=true'.split(' ')
|
||||
const options = await cypress.cli.parseRunArguments(args)
|
||||
|
||||
// we don't need to convert the environment into an object
|
||||
// since the logic inside cypress.run handles that
|
||||
expect(options).to.deep.equal({
|
||||
env: 'MY_NUMBER=42,MY_FLAG=true',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
25
cli/types/cypress-npm-api.d.ts
vendored
25
cli/types/cypress-npm-api.d.ts
vendored
@@ -306,6 +306,25 @@ declare module 'cypress' {
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods allow parsing given CLI arguments the same way Cypress CLI does it.
|
||||
*/
|
||||
interface CypressCliParser {
|
||||
/**
|
||||
* Parses the given array of string arguments to "cypress run"
|
||||
* just like Cypress CLI does it.
|
||||
* @see https://on.cypress.io/module-api
|
||||
* @example
|
||||
* const cypress = require('cypress')
|
||||
* const args = ['cypress', 'run', '--browser', 'chrome']
|
||||
* const options = await cypress.cli.parseRunArguments(args)
|
||||
* // options is {browser: 'chrome'}
|
||||
* // pass the options to cypress.run()
|
||||
* const results = await cypress.run(options)
|
||||
*/
|
||||
parseRunArguments(args: string[]): Promise<Partial<CypressRunOptions>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Cypress NPM module interface.
|
||||
* @see https://on.cypress.io/module-api
|
||||
@@ -337,6 +356,12 @@ declare module 'cypress' {
|
||||
* @see https://on.cypress.io/module-api#cypress-open
|
||||
*/
|
||||
open(options?: Partial<CypressOpenOptions>): Promise<void>
|
||||
|
||||
/**
|
||||
* Utility functions for parsing CLI arguments the same way
|
||||
* Cypress does
|
||||
*/
|
||||
cli: CypressCliParser
|
||||
}
|
||||
|
||||
// export Cypress NPM module interface
|
||||
|
||||
Reference in New Issue
Block a user