cli: expose method that parses cypress run CLI logic (#7798)

Co-authored-by: Zach Bloomquist <github@chary.us>
This commit is contained in:
Gleb Bahmutov
2020-07-14 15:39:17 -04:00
committed by GitHub
parent bf272a6d8f
commit ef2363ea78
5 changed files with 242 additions and 36 deletions

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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',
})
})
})
})
})

View File

@@ -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