Skip to content

Commit

Permalink
Merge branch 'develop' into v5.0-release
Browse files Browse the repository at this point in the history
  • Loading branch information
flotwig committed Jun 23, 2020
2 parents 946b8c4 + 653739b commit 8a6af2b
Show file tree
Hide file tree
Showing 42 changed files with 556 additions and 96 deletions.
2 changes: 1 addition & 1 deletion LICENSE
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Cypress.io
Copyright (c) 2020 Cypress.io

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 1 addition & 0 deletions cli/__snapshots__/cli_spec.js
Expand Up @@ -75,6 +75,7 @@ exports['shows help for run --foo 1'] = `
--parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes
-p, --port <port> runs Cypress on a specific port. overrides any value in cypress.json.
-P, --project <project-path> path to the project
-q, --quiet run quietly, using only the configured reporter
--record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard.
-r, --reporter <reporter> runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"
-o, --reporter-options <reporter-options> options for the mocha reporter. defaults to "null"
Expand Down
2 changes: 2 additions & 0 deletions cli/__snapshots__/errors_spec.js
Expand Up @@ -35,6 +35,7 @@ exports['errors individual has the following errors 1'] = [
"incompatibleHeadlessFlags",
"invalidCacheDirectory",
"invalidCypressEnv",
"invalidRunProjectPath",
"invalidSmokeTestDisplayError",
"missingApp",
"missingDependency",
Expand All @@ -44,6 +45,7 @@ exports['errors individual has the following errors 1'] = [
"removed",
"smokeTestFailure",
"unexpected",
"unknownError",
"versionMismatch"
]

Expand Down
2 changes: 2 additions & 0 deletions cli/lib/cli.js
Expand Up @@ -109,6 +109,7 @@ const descriptions = {
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.json.',
project: 'path to the project',
quiet: 'run quietly, using only the configured reporter',
record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
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"',
Expand Down Expand Up @@ -231,6 +232,7 @@ module.exports = {
.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'))
Expand Down
12 changes: 12 additions & 0 deletions cli/lib/cypress.js
Expand Up @@ -9,13 +9,25 @@ const run = require('./exec/run')
const util = require('./util')

const cypressModuleApi = {
/**
* Opens Cypress GUI
* @see https://on.cypress.io/module-api#cypress-open
*/
open (options = {}) {
options = util.normalizeModuleOptions(options)

return open.start(options)
},

/**
* Runs Cypress tests in the current project
* @see https://on.cypress.io/module-api#cypress-run
*/
run (options = {}) {
if (!run.isValidProject(options.project)) {
return Promise.reject(new Error(`Invalid project path parameter: ${options.project}`))
}

options = util.normalizeModuleOptions(options)

return tmp.fileAsync()
Expand Down
31 changes: 26 additions & 5 deletions cli/lib/errors.js
Expand Up @@ -9,13 +9,36 @@ const state = require('./tasks/state')

const docsUrl = 'https://on.cypress.io'
const requiredDependenciesUrl = `${docsUrl}/required-dependencies`
const runDocumentationUrl = `${docsUrl}/cypress-run`

// TODO it would be nice if all error objects could be enforced via types
// to only have description + solution properties

const hr = '----------'

const genericErrorSolution = stripIndent`
Search for an existing issue or open a GitHub issue at
${chalk.blue(util.issuesUrl)}
`

// common errors Cypress application can encounter
const unknownError = {
description: 'Unknown Cypress CLI error',
solution: genericErrorSolution,
}

const invalidRunProjectPath = {
description: 'Invalid --project path',
solution: stripIndent`
Please provide a valid project path.
Learn more about ${chalk.cyan('cypress run')} at:
${chalk.blue(runDocumentationUrl)}
`,
}

const failedDownload = {
description: 'The Cypress App could not be downloaded.',
solution: stripIndent`
Expand All @@ -26,11 +49,7 @@ const failedDownload = {

const failedUnzip = {
description: 'The Cypress App could not be unzipped.',
solution: stripIndent`
Search for an existing issue or open a GitHub issue at
${chalk.blue(util.issuesUrl)}
`,
solution: genericErrorSolution,
}

const missingApp = (binaryDir) => {
Expand Down Expand Up @@ -390,6 +409,7 @@ module.exports = {
getError,
hr,
errors: {
unknownError,
nonZeroExitCodeXvfb,
missingXvfb,
missingApp,
Expand All @@ -408,5 +428,6 @@ module.exports = {
smokeTestFailure,
childProcessKilled,
incompatibleHeadlessFlags,
invalidRunProjectPath,
},
}
65 changes: 57 additions & 8 deletions cli/lib/exec/run.js
Expand Up @@ -6,11 +6,60 @@ const spawn = require('./spawn')
const verify = require('../tasks/verify')
const { exitWithError, errors } = require('../errors')

// maps options collected by the CLI
// and forms list of CLI arguments to the server
/**
* Throws an error with "details" property from
* "errors" object.
* @param {Object} details - Error details
*/
const throwInvalidOptionError = (details) => {
if (!details) {
details = errors.unknownError
}

// throw this error synchronously, it will be caught later on and
// the details will be propagated to the promise chain
const err = new Error()

err.details = details
throw err
}

/**
* Typically a user passes a string path to the project.
* But "cypress open" allows using `false` to open in global mode,
* and the user can accidentally execute `cypress run --project false`
* which should be invalid.
*/
const isValidProject = (v) => {
if (typeof v === 'boolean') {
return false
}

if (v === '' || v === 'false' || v === 'true') {
return false
}

return true
}

/**
* Maps options collected by the CLI
* and forms list of CLI arguments to the server.
*
* Note: there is lightweight validation, with errors
* thrown synchronously.
*
* @returns {string[]} list of CLI arguments
*/
const processRunOptions = (options = {}) => {
debug('processing run options %o', options)

if (!isValidProject(options.project)) {
debug('invalid project option %o', { project: options.project })

return throwInvalidOptionError(errors.invalidRunProjectPath)
}

const args = ['--run-project', options.project]

if (options.browser) {
Expand Down Expand Up @@ -55,12 +104,7 @@ const processRunOptions = (options = {}) => {

if (options.headless) {
if (options.headed) {
// throw this error synchronously, it will be caught later on and
// the details will be propagated to the promise chain
const err = new Error()

err.details = errors.incompatibleHeadlessFlags
throw err
return throwInvalidOptionError(errors.incompatibleHeadlessFlags)
}

args.push('--headed', !options.headless)
Expand Down Expand Up @@ -89,6 +133,10 @@ const processRunOptions = (options = {}) => {
args.push('--port', options.port)
}

if (options.quiet) {
args.push('--quiet')
}

// if record is defined and we're not
// already in ci mode, then send it up
if (options.record != null && !options.ci) {
Expand Down Expand Up @@ -119,6 +167,7 @@ const processRunOptions = (options = {}) => {

module.exports = {
processRunOptions,
isValidProject,
// resolves with the number of failed tests
start (options = {}) {
_.defaults(options, {
Expand Down
1 change: 1 addition & 0 deletions cli/lib/util.js
Expand Up @@ -214,6 +214,7 @@ const parseOpts = (opts) => {
'parallel',
'port',
'project',
'quiet',
'reporter',
'reporterOptions',
'record',
Expand Down
24 changes: 24 additions & 0 deletions cli/test/lib/cypress_spec.js
Expand Up @@ -147,5 +147,29 @@ describe('cypress', function () {
expect(args).to.deep.eq(opts)
})
})

it('rejects if project is an empty string', () => {
return expect(cypress.run({ project: '' })).to.be.rejected
})

it('rejects if project is true', () => {
return expect(cypress.run({ project: true })).to.be.rejected
})

it('rejects if project is false', () => {
return expect(cypress.run({ project: false })).to.be.rejected
})

it('passes quiet: true', () => {
const opts = {
quiet: true,
}

return cypress.run(opts)
.then(getStartArgs)
.then((args) => {
expect(args).to.deep.eq(opts)
})
})
})
})
22 changes: 22 additions & 0 deletions cli/test/lib/exec/run_spec.js
Expand Up @@ -15,6 +15,28 @@ describe('exec run', function () {
})

context('.processRunOptions', function () {
it('allows string --project option', () => {
const args = run.processRunOptions({
project: '/path/to/project',
})

expect(args).to.deep.equal(['--run-project', '/path/to/project'])
})

it('throws an error for empty string --project', () => {
expect(() => run.processRunOptions({ project: '' })).to.throw()
})

it('throws an error for boolean --project', () => {
expect(() => run.processRunOptions({ project: false })).to.throw()
expect(() => run.processRunOptions({ project: true })).to.throw()
})

it('throws an error for --project "false" or "true"', () => {
expect(() => run.processRunOptions({ project: 'false' })).to.throw()
expect(() => run.processRunOptions({ project: 'true' })).to.throw()
})

it('passes --browser option', () => {
const args = run.processRunOptions({
browser: 'test browser',
Expand Down
4 changes: 4 additions & 0 deletions cli/types/cypress-npm-api.d.ts
Expand Up @@ -68,6 +68,10 @@ declare module 'cypress' {
* Override default port
*/
port: number
/**
* Run quietly, using only the configured reporter
*/
quiet: boolean
/**
* Whether to record the test run
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop-gui/cypress/integration/login_spec.js
Expand Up @@ -69,6 +69,12 @@ describe('Login', function () {
})
})

it('passes utm code when it triggers ipc \'begin:auth\'', function () {
cy.then(function () {
expect(this.ipc.beginAuth).to.be.calledWith('Nav Login Button')
})
})

it('disables login button', () => {
cy.get('@loginBtn').should('be.disabled')
})
Expand Down
14 changes: 13 additions & 1 deletion packages/desktop-gui/cypress/integration/runs_list_spec.js
Expand Up @@ -303,6 +303,18 @@ describe('Runs List', function () {
it('does not fetch runs', function () {
expect(this.ipc.getRuns).not.to.be.called
})

it('clicking Log In to Dashboard opens login', () => {
cy.contains('button', 'Log In to Dashboard').click().then(function () {
expect(this.ipc.beginAuth).to.be.calledOnce
})
})

it('clicking Log In to Dashboard passes utm code', () => {
cy.contains('button', 'Log In to Dashboard').click().then(function () {
expect(this.ipc.beginAuth).to.be.calledWith('Runs Tab Login Button')
})
})
})

context('without a project id', function () {
Expand Down Expand Up @@ -345,7 +357,7 @@ describe('Runs List', function () {

it('clicking Log In to Dashboard opens login', () => {
cy.contains('button', 'Log In to Dashboard').click().then(function () {
expect(this.ipc.beginAuth).to.be.called
expect(this.ipc.beginAuth).to.be.calledOnce
})
})
})
Expand Down
4 changes: 2 additions & 2 deletions packages/desktop-gui/src/auth/auth-api.js
Expand Up @@ -21,12 +21,12 @@ class AuthApi {
})
}

login () {
login (utm) {
ipc.onAuthMessage((__, message) => {
authStore.setMessage(message)
})

return ipc.beginAuth()
return ipc.beginAuth(utm)
.then((user) => {
authStore.setUser(user)
authStore.setMessage(null)
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop-gui/src/auth/login-form.jsx
Expand Up @@ -115,7 +115,7 @@ class LoginForm extends Component {

this.setState({ isLoggingIn: true })

authApi.login()
authApi.login(this.props.utm)
.then(() => {
this.props.onSuccess()
})
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop-gui/src/auth/login-modal.jsx
Expand Up @@ -70,7 +70,7 @@ class LoginContent extends Component {
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<h1><i className='fas fa-lock'></i> Log In</h1>
<p>Logging in gives you access to the <a onClick={this._openDashboard}>Cypress Dashboard Service</a>. You can set up projects to be recorded and see test data from your project.</p>
<LoginForm onSuccess={() => this.setState({ succeeded: true })} />
<LoginForm utm='Nav Login Button' onSuccess={() => this.setState({ succeeded: true })} />
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop-gui/src/runs/runs-list.jsx
Expand Up @@ -284,7 +284,7 @@ class RunsList extends Component {
<img width='150' height='150' src='https://on.cypress.io/images/desktop-onboarding-thumb-3' />
</div>

<LoginForm />
<LoginForm utm='Runs Tab Login Button' />
</div>
)
}
Expand Down

0 comments on commit 8a6af2b

Please sign in to comment.