diff --git a/cli/__snapshots__/cache_spec.js b/cli/__snapshots__/cache_spec.js new file mode 100644 index 0000000000..2811dd1a12 --- /dev/null +++ b/cli/__snapshots__/cache_spec.js @@ -0,0 +1,11 @@ +exports['lib/tasks/cache .clear deletes cache folder and everything inside it 1'] = ` +[no output] +` + +exports['lib/tasks/cache .list lists all versions of cached binary 1'] = ` +1.2.3, 2.3.4 +` + +exports['lib/tasks/cache .path lists path to cache 1'] = ` +/.cache/Cypress +` diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 568506ede0..dad437b155 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -255,3 +255,99 @@ exports['cli -v no binary version 1'] = ` Cypress package version: 1.2.3 Cypress binary version: not installed ` + +exports['cli unknown option shows help for cache command - unknown option --foo 1'] = ` + + command: bin/cypress cache --foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + error: unknown option: --foo + + + Usage: cache [command] + + Manages the Cypress binary cache + + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + +exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = ` + + command: bin/cypress cache foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + error: unknown command: cache foo + + + Usage: cache [command] + + Manages the Cypress binary cache + + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + +exports['cli unknown option shows help for cache command - no sub-command 1'] = ` + + command: bin/cypress cache + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + Usage: cache [command] + + Manages the Cypress binary cache + + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 48914b732e..9f7f147ec3 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -15,8 +15,7 @@ function unknownOption (flag, type = 'option') { logger.error(` error: unknown ${type}:`, flag) logger.error() this.outputHelp() - logger.error() - process.exit(1) + util.exit(1) } commander.Command.prototype.unknownOption = unknownOption @@ -62,9 +61,9 @@ const descriptions = { 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 cypress binary cache path', - cacheList: 'list the currently cached versions', - cacheClear: 'delete the Cypress binary cache', + 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', @@ -203,8 +202,13 @@ module.exports = { .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}`, 'sub-command') + unknownOption.call(this, `cache ${opts}`, 'command') } cache[opts]() diff --git a/cli/test/lib/cli_spec.js b/cli/test/lib/cli_spec.js index c88e179307..cc2db136b1 100644 --- a/cli/test/lib/cli_spec.js +++ b/cli/test/lib/cli_spec.js @@ -1,5 +1,6 @@ require('../spec_helper') +const os = require('os') const cli = require(`${lib}/cli`) const util = require(`${lib}/util`) const logger = require(`${lib}/logger`) @@ -11,13 +12,14 @@ const install = require(`${lib}/tasks/install`) const snapshot = require('snap-shot-it') const execa = require('execa-wrap') -describe('cli', function () { +describe('cli', () => { require('mocha-banner').register() - beforeEach(function () { + beforeEach(() => { logger.reset() sinon.stub(process, 'exit') - sinon.stub(util, 'exit') + os.platform.returns('darwin') + // sinon.stub(util, 'exit') sinon.stub(util, 'logErrorExit1') this.exec = (args) => { return cli.init(`node test ${args}`.split(' ')) @@ -30,48 +32,54 @@ describe('cli', function () { return execa('bin/cypress', ['open', '--foo']).then((result) => { snapshot('shows help for open --foo', result) }) - } - ) + }) it('shows help for run command', () => { return execa('bin/cypress', ['run', '--foo']).then((result) => { snapshot('shows help for run --foo', result) }) - } - ) + }) + + it('shows help for cache command - unknown option --foo', () => { + return execa('bin/cypress', ['cache', '--foo']).then(snapshot) + }) + + it('shows help for cache command - unknown sub-command foo', () => { + return execa('bin/cypress', ['cache', 'foo']).then(snapshot) + }) + + it('shows help for cache command - no sub-command', () => { + return execa('bin/cypress', ['cache']).then(snapshot) + }) }) context('help command', () => { it('shows help', () => { return execa('bin/cypress', ['help']).then(snapshot) - } - ) + }) it('shows help for -h', () => { return execa('bin/cypress', ['-h']).then(snapshot) - } - ) + }) it('shows help for --help', () => { return execa('bin/cypress', ['--help']).then(snapshot) - } - ) + }) }) context('unknown command', () => { it('shows usage and exits', () => { return execa('bin/cypress', ['foo']).then(snapshot) - } - ) + }) }) - context('cypress version', function () { + context('cypress version', () => { const binaryDir = '/binary/dir' - beforeEach(function () { + beforeEach(() => { sinon.stub(state, 'getBinaryDir').returns(binaryDir) }) - it('reports package version', function (done) { + it('reports package version', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon.stub(state, 'getBinaryPkgVersionAsync').withArgs(binaryDir).resolves('X.Y.Z') @@ -82,7 +90,7 @@ describe('cli', function () { }) }) - it('reports package and binary message', function (done) { + it('reports package and binary message', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon.stub(state, 'getBinaryPkgVersionAsync').resolves('X.Y.Z') @@ -93,7 +101,7 @@ describe('cli', function () { }) }) - it('handles non-existent binary version', function (done) { + it('handles non-existent binary version', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) @@ -104,7 +112,7 @@ describe('cli', function () { }) }) - it('handles non-existent binary --version', function (done) { + it('handles non-existent binary --version', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) @@ -115,7 +123,7 @@ describe('cli', function () { }) }) - it('handles non-existent binary -v', function (done) { + it('handles non-existent binary -v', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon.stub(state, 'getBinaryPkgVersionAsync').resolves(null) @@ -127,13 +135,13 @@ describe('cli', function () { }) }) - context('cypress run', function () { - beforeEach(function () { + context('cypress run', () => { + beforeEach(() => { sinon.stub(run, 'start').resolves(0) - util.exit.withArgs(0) + sinon.stub(util, 'exit').withArgs(0) }) - it('calls run.start with options + exits with code', function (done) { + it('calls run.start with options + exits with code', (done) => { run.start.resolves(10) this.exec('run') @@ -143,7 +151,7 @@ describe('cli', function () { }) }) - it('run.start with options + catches errors', function (done) { + it('run.start with options + catches errors', (done) => { const err = new Error('foo') run.start.rejects(err) @@ -155,110 +163,110 @@ describe('cli', function () { }) }) - it('calls run with port', function () { + it('calls run with port', () => { this.exec('run --port 7878') expect(run.start).to.be.calledWith({ port: '7878' }) }) - it('calls run with spec', function () { + it('calls run with spec', () => { this.exec('run --spec cypress/integration/foo_spec.js') expect(run.start).to.be.calledWith({ spec: 'cypress/integration/foo_spec.js' }) }) - it('calls run with port with -p arg', function () { + it('calls run with port with -p arg', () => { this.exec('run -p 8989') expect(run.start).to.be.calledWith({ port: '8989' }) }) - it('calls run with env variables', function () { + it('calls run with env variables', () => { this.exec('run --env foo=bar,host=http://localhost:8888') expect(run.start).to.be.calledWith({ env: 'foo=bar,host=http://localhost:8888' }) }) - it('calls run with config', function () { + it('calls run with config', () => { this.exec('run --config watchForFileChanges=false,baseUrl=localhost') expect(run.start).to.be.calledWith({ config: 'watchForFileChanges=false,baseUrl=localhost' }) }) - it('calls run with key', function () { + it('calls run with key', () => { this.exec('run --key asdf') expect(run.start).to.be.calledWith({ key: 'asdf' }) }) - it('calls run with --record', function () { + it('calls run with --record', () => { this.exec('run --record') expect(run.start).to.be.calledWith({ record: true }) }) - it('calls run with --record false', function () { + it('calls run with --record false', () => { this.exec('run --record false') expect(run.start).to.be.calledWith({ record: false }) }) - it('calls run with relative --project folder', function () { + it('calls run with relative --project folder', () => { this.exec('run --project foo/bar') expect(run.start).to.be.calledWith({ project: 'foo/bar' }) }) - it('calls run with absolute --project folder', function () { + it('calls run with absolute --project folder', () => { this.exec('run --project /tmp/foo/bar') expect(run.start).to.be.calledWith({ project: '/tmp/foo/bar' }) }) - it('calls run with headed', function () { + it('calls run with headed', () => { this.exec('run --headed') expect(run.start).to.be.calledWith({ headed: true }) }) - it('calls run with --no-exit', function () { + it('calls run with --no-exit', () => { this.exec('run --no-exit') expect(run.start).to.be.calledWith({ exit: false }) }) - it('calls run with --parallel', function () { + it('calls run with --parallel', () => { this.exec('run --parallel') expect(run.start).to.be.calledWith({ parallel: true }) }) - it('calls runs with --ci-build-id', function () { + it('calls runs with --ci-build-id', () => { this.exec('run --ci-build-id 123') expect(run.start).to.be.calledWith({ ciBuildId: '123' }) }) - it('calls runs with --group', function () { + it('calls runs with --group', () => { this.exec('run --group staging') expect(run.start).to.be.calledWith({ group: 'staging' }) }) }) - context('cypress open', function () { - beforeEach(function () { + context('cypress open', () => { + beforeEach(() => { sinon.stub(open, 'start').resolves(0) }) - it('calls open.start with relative --project folder', function () { + it('calls open.start with relative --project folder', () => { this.exec('open --project foo/bar') expect(open.start).to.be.calledWith({ project: 'foo/bar' }) }) - it('calls open.start with absolute --project folder', function () { + it('calls open.start with absolute --project folder', () => { this.exec('open --project /tmp/foo/bar') expect(open.start).to.be.calledWith({ project: '/tmp/foo/bar' }) }) - it('calls open.start with options', function () { + it('calls open.start with options', () => { // sinon.stub(open, 'start').resolves() this.exec('open --port 7878') expect(open.start).to.be.calledWith({ port: '7878' }) }) - it('calls open.start with global', function () { + it('calls open.start with global', () => { // sinon.stub(open, 'start').resolves() this.exec('open --port 7878 --global') expect(open.start).to.be.calledWith({ port: '7878', global: true }) }) - it('calls open.start + catches errors', function (done) { + it('calls open.start + catches errors', (done) => { const err = new Error('foo') open.start.rejects(err) @@ -271,19 +279,19 @@ describe('cli', function () { }) }) - it('install calls install.start without forcing', function () { + it('install calls install.start without forcing', () => { sinon.stub(install, 'start').resolves() this.exec('install') expect(install.start).not.to.be.calledWith({ force: true }) }) - it('install calls install.start with force: true when passed', function () { + it('install calls install.start with force: true when passed', () => { sinon.stub(install, 'start').resolves() this.exec('install --force') expect(install.start).to.be.calledWith({ force: true }) }) - it('install calls install.start + catches errors', function (done) { + it('install calls install.start + catches errors', (done) => { const err = new Error('foo') sinon.stub(install, 'start').rejects(err) @@ -294,15 +302,15 @@ describe('cli', function () { done() }) }) - context('cypress verify', function () { + context('cypress verify', () => { - it('verify calls verify.start with force: true', function () { + it('verify calls verify.start with force: true', () => { sinon.stub(verify, 'start').resolves() this.exec('verify') expect(verify.start).to.be.calledWith({ force: true, welcomeMessage: false }) }) - it('verify calls verify.start + catches errors', function (done) { + it('verify calls verify.start + catches errors', (done) => { const err = new Error('foo') sinon.stub(verify, 'start').rejects(err) diff --git a/cli/test/lib/tasks/cache_spec.js b/cli/test/lib/tasks/cache_spec.js index 6ce1f42a09..62d1df6a6d 100644 --- a/cli/test/lib/tasks/cache_spec.js +++ b/cli/test/lib/tasks/cache_spec.js @@ -6,6 +6,7 @@ const fs = require(`${lib}/fs`) const state = require(`${lib}/tasks/state`) const cache = require(`${lib}/tasks/cache`) const stdout = require('../../support/stdout') +const snapshot = require('snap-shot-it') describe('lib/tasks/cache', () => { beforeEach(() => { @@ -24,6 +25,9 @@ describe('lib/tasks/cache', () => { }) afterEach(() => { + mockfs.restore() + this.stdout = this.stdout.toString().split('\n').slice(0, -2).join('\n') + snapshot(this.stdout.toString() || '[no output]') stdout.restore() }) diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 46bb20e65e..c8231f7087 100644 --- a/cli/test/lib/tasks/download_spec.js +++ b/cli/test/lib/tasks/download_spec.js @@ -45,28 +45,33 @@ describe('lib/tasks/download', function () { context('download url', () => { it('returns url', () => { const url = download.getUrl() + la(is.url(url), url) }) it('returns latest desktop url', () => { const url = download.getUrl() + snapshot('latest desktop url', normalize(url)) }) it('returns specific desktop version url', () => { const url = download.getUrl('0.20.2') + snapshot('specific version desktop url', normalize(url)) }) it('returns input if it is already an https link', () => { const url = 'https://somewhere.com' const result = download.getUrl(url) + expect(result).to.equal(url) }) it('returns input if it is already an http link', () => { const url = 'http://local.com' const result = download.getUrl(url) + expect(result).to.equal(url) }) }) @@ -75,24 +80,28 @@ describe('lib/tasks/download', function () { it('env var', () => { process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com' const url = download.getUrl('0.20.2') + snapshot('base url from CYPRESS_DOWNLOAD_MIRROR', normalize(url)) }) it('env var with trailing slash', () => { process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/' const url = download.getUrl('0.20.2') + snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with trailing slash', normalize(url)) }) it('env var with subdirectory', () => { process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/example' const url = download.getUrl('0.20.2') + snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory', normalize(url)) }) it('env var with subdirectory and trailing slash', () => { process.env.CYPRESS_DOWNLOAD_MIRROR = 'https://cypress.example.com/example/' const url = download.getUrl('0.20.2') + snapshot('base url from CYPRESS_DOWNLOAD_MIRROR with subdirectory and trailing slash', normalize(url)) }) }) @@ -100,7 +109,9 @@ describe('lib/tasks/download', function () { it('saves example.zip to options.downloadDestination', function () { nock('https://aws.amazon.com') .get('/some.zip') - .reply(200, () => fs.createReadStream('test/fixture/example.zip')) + .reply(200, () => { + return fs.createReadStream('test/fixture/example.zip') + }) nock('https://download.cypress.io') .get('/desktop/1.2.3') @@ -110,7 +121,6 @@ describe('lib/tasks/download', function () { 'x-version': '0.11.1', }) - const onProgress = sinon.stub().returns(undefined) return download.start({ @@ -120,6 +130,7 @@ describe('lib/tasks/download', function () { }) .then((responseVersion) => { expect(responseVersion).to.eq('0.11.1') + return fs.statAsync(downloadDestination) }) }) @@ -127,7 +138,9 @@ describe('lib/tasks/download', function () { it('resolves with response x-version if present', function () { nock('https://aws.amazon.com') .get('/some.zip') - .reply(200, () => fs.createReadStream('test/fixture/example.zip')) + .reply(200, () => { + return fs.createReadStream('test/fixture/example.zip') + }) nock('https://download.cypress.io') .get('/desktop/1.2.3') @@ -147,7 +160,9 @@ describe('lib/tasks/download', function () { nock('https://aws.amazon.com') .get('/some.zip') - .reply(200, () => fs.createReadStream('test/fixture/example.zip')) + .reply(200, () => { + return fs.createReadStream('test/fixture/example.zip') + }) nock('https://download.cypress.io') .get('/desktop/0.13.0') @@ -159,6 +174,7 @@ describe('lib/tasks/download', function () { return download.start(this.options).then((responseVersion) => { expect(responseVersion).to.eq('0.13.0') + return fs.statAsync(downloadDestination) }) }) @@ -167,6 +183,7 @@ describe('lib/tasks/download', function () { const ctx = this const err = new Error() + err.statusCode = 404 err.statusMessage = 'Not Found' this.options.version = null diff --git a/packages/driver/src/cy/commands/request.coffee b/packages/driver/src/cy/commands/request.coffee index 1d13a13a64..9e6a06cc32 100644 --- a/packages/driver/src/cy/commands/request.coffee +++ b/packages/driver/src/cy/commands/request.coffee @@ -21,6 +21,7 @@ REQUEST_DEFAULTS = { json: null form: null gzip: true + timeout: null followRedirect: true } @@ -73,11 +74,13 @@ module.exports = (Commands, Cypress, cy, state, config) -> o.body = args[2] _.defaults(options, REQUEST_DEFAULTS, { - log: true - timeout: config("responseTimeout") + log: true, failOnStatusCode: true }) + ## if timeout is not supplied, use the configured default + options.timeout ||= config("responseTimeout") + options.method = options.method.toUpperCase() if _.has(options, "failOnStatus") diff --git a/packages/driver/test/cypress/integration/commands/request_spec.coffee b/packages/driver/test/cypress/integration/commands/request_spec.coffee index 84a632bcd9..b560ad2921 100644 --- a/packages/driver/test/cypress/integration/commands/request_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/request_spec.coffee @@ -1,10 +1,12 @@ _ = Cypress._ Promise = Cypress.Promise +RESPONSE_TIMEOUT = 22222 describe "src/cy/commands/request", -> context "#request", -> beforeEach -> cy.stub(Cypress, "backend").callThrough() + Cypress.config("responseTimeout", RESPONSE_TIMEOUT) describe "argument signature", -> beforeEach -> @@ -27,6 +29,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "accepts object with url, method, headers, body", -> @@ -48,6 +51,17 @@ describe "src/cy/commands/request", -> headers: { "x-token": "abc123" } + timeout: RESPONSE_TIMEOUT + }) + + it "accepts object with url + timeout", -> + cy.request({url: "http://localhost:8000/foo", timeout: 23456}).then -> + @expectOptionsToBe({ + url: "http://localhost:8000/foo" + method: "GET" + gzip: true + followRedirect: true + timeout: 23456 }) it "accepts string url", -> @@ -57,6 +71,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "accepts method + url", -> @@ -66,6 +81,7 @@ describe "src/cy/commands/request", -> method: "DELETE" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "accepts method + url + body", -> @@ -77,6 +93,7 @@ describe "src/cy/commands/request", -> json: true gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "accepts url + body", -> @@ -88,6 +105,7 @@ describe "src/cy/commands/request", -> json: true gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "accepts url + string body", -> @@ -98,6 +116,7 @@ describe "src/cy/commands/request", -> body: "foo" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) context "method normalization", -> @@ -108,6 +127,7 @@ describe "src/cy/commands/request", -> method: "POST" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) context "url normalization", -> @@ -120,6 +140,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "uses localhost urls", -> @@ -129,6 +150,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "uses wwww urls", -> @@ -138,6 +160,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "prefixes with baseUrl when origin is empty", -> @@ -150,6 +173,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "prefixes with baseUrl over current origin", -> @@ -162,6 +186,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) context "gzip", -> @@ -175,6 +200,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: false followRedirect: true + timeout: RESPONSE_TIMEOUT }) context "auth", -> @@ -195,6 +221,7 @@ describe "src/cy/commands/request", -> user: "brian" pass: "password" } + timeout: RESPONSE_TIMEOUT }) context "followRedirect", -> @@ -206,6 +233,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: true + timeout: RESPONSE_TIMEOUT }) it "can be set to false", -> @@ -219,6 +247,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: false + timeout: RESPONSE_TIMEOUT }) it "normalizes followRedirects -> followRedirect", -> @@ -232,6 +261,7 @@ describe "src/cy/commands/request", -> method: "GET" gzip: true followRedirect: false + timeout: RESPONSE_TIMEOUT }) context "qs", -> @@ -249,6 +279,7 @@ describe "src/cy/commands/request", -> gzip: true followRedirect: true qs: {foo: "bar"} + timeout: RESPONSE_TIMEOUT }) context "form", -> @@ -268,6 +299,7 @@ describe "src/cy/commands/request", -> form: true followRedirect: true body: {foo: "bar"} + timeout: RESPONSE_TIMEOUT }) it "accepts a string for body", -> @@ -284,6 +316,7 @@ describe "src/cy/commands/request", -> form: true followRedirect: true body: "foo=bar&baz=quux" + timeout: RESPONSE_TIMEOUT }) describe "failOnStatus", ->