diff --git a/circle.yml b/circle.yml index 5413b5381363..c16b702f5d75 100644 --- a/circle.yml +++ b/circle.yml @@ -85,9 +85,9 @@ jobs: - restore_cache: key: v5-{{ arch }}-{{ .Branch }}-deps-launcher - restore_cache: - key: v5-{{ arch }}-{{ .Branch }}-deps-reporter + key: v8-{{ arch }}-{{ .Branch }}-deps-reporter - restore_cache: - key: v5-{{ arch }}-{{ .Branch }}-deps-runner + key: v8-{{ arch }}-{{ .Branch }}-deps-runner - restore_cache: key: v6-{{ arch }}-{{ .Branch }}-deps-server - restore_cache: @@ -159,11 +159,11 @@ jobs: paths: - packages/launcher/node_modules - save_cache: - key: v5-{{ arch }}-{{ .Branch }}-deps-reporter-{{ checksum "packages/reporter/package.json" }} + key: v8-{{ arch }}-{{ .Branch }}-deps-reporter-{{ checksum "packages/reporter/package.json" }} paths: - packages/reporter/node_modules - save_cache: - key: v5-{{ arch }}-{{ .Branch }}-deps-runner-{{ checksum "packages/runner/package.json" }} + key: v8-{{ arch }}-{{ .Branch }}-deps-runner-{{ checksum "packages/runner/package.json" }} paths: - packages/runner/node_modules - save_cache: @@ -228,6 +228,16 @@ jobs: - store_test_results: path: /tmp/cypress + lint-typescript: + <<: *defaults + parallelism: 1 + steps: + - attach_workspace: + at: ~/ + - run: + command: npm run dtslint + working_directory: cli + "server-unit-tests": <<: *defaults parallelism: 2 @@ -677,6 +687,9 @@ linux-workflow: &linux-workflow name: Linux lint requires: - build + - lint-typescript: + requires: + - build # unit, integration and e2e tests - unit-tests: requires: diff --git a/cli/package.json b/cli/package.json index 773447438bb6..5cc9677da6b1 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,7 +13,7 @@ "pretest": "npm run check-deps-pre", "test": "npm run test-unit", "pretest-unit": "npm run check-deps-pre", - "test-unit": "npm run dtslint && npm run unit", + "test-unit": "npm run unit", "pretest-watch": "npm run check-deps-pre", "test-watch": "npm run unit -- --watch", "check-deps": "node ../scripts/check-deps.js --verbose", @@ -88,7 +88,7 @@ "chai-string": "1.4.0", "clear-module": "2.1.0", "dependency-check": "2.10.1", - "dtslint": "0.5.0", + "dtslint": "0.5.1", "execa-wrap": "1.4.0", "mock-fs": "4.8.0", "nock": "9.6.1", diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index c5fe14ccaf95..7a7a2220cfa6 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -1588,9 +1588,14 @@ declare namespace Cypress { * @example * cy.visit('http://localhost:3000') * cy.visit('/somewhere') // opens ${baseUrl}/somewhere + * cy.visit({ + * url: 'http://google.com', + * method: 'POST' + * }) * */ visit(url: string, options?: Partial): Chainable + visit(options: Partial & { url: string }): Chainable /** * Wait for a number of milliseconds. diff --git a/packages/desktop-gui/src/specs/specs.scss b/packages/desktop-gui/src/specs/specs.scss index 5990c7923bec..4c9b1ca8bdef 100644 --- a/packages/desktop-gui/src/specs/specs.scss +++ b/packages/desktop-gui/src/specs/specs.scss @@ -144,7 +144,7 @@ } .file > a { - font-weight: 300; + font-weight: 400; border-bottom: 1px dotted #eeeeee; padding: 4px 0; font-family: $font-sans; diff --git a/packages/driver/src/cy/commands/navigation.coffee b/packages/driver/src/cy/commands/navigation.coffee index 4ca568774140..c21b4006a9fe 100644 --- a/packages/driver/src/cy/commands/navigation.coffee +++ b/packages/driver/src/cy/commands/navigation.coffee @@ -28,6 +28,11 @@ reset = (test = {}) -> id = test.id +VALID_VISIT_METHODS = ['GET', 'POST'] + +isValidVisitMethod = (method) -> + _.includes(VALID_VISIT_METHODS, method) + timedOutWaitingForPageLoad = (ms, log) -> $utils.throwErrByPath("navigation.timed_out", { onFail: log @@ -260,7 +265,7 @@ module.exports = (Commands, Cypress, cy, state, config) -> Cypress.backend( "resolve:url", url, - _.pick(options, "failOnStatusCode", "auth") + _.pick(options, "auth", "failOnStatusCode", "method", "body", "headers") ) .then (resp = {}) -> switch @@ -456,22 +461,45 @@ module.exports = (Commands, Cypress, cy, state, config) -> $utils.throwErrByPath("go.invalid_argument", { onFail: options._log }) visit: (url, options = {}) -> + if options.url and url + $utils.throwErrByPath("visit.no_duplicate_url", { args: { optionsUrl: options.url, url: url }}) + + if _.isObject(url) and _.isEqual(options, {}) + ## options specified as only argument + options = url + url = options.url + if not _.isString(url) $utils.throwErrByPath("visit.invalid_1st_arg") _.defaults(options, { auth: null failOnStatusCode: true + method: 'GET' + body: null + headers: {} log: true timeout: config("pageLoadTimeout") onBeforeLoad: -> onLoad: -> }) + if not isValidVisitMethod(options.method) + $utils.throwErrByPath("visit.invalid_method", { args: { method: options.method }}) + + if not _.isObject(options.headers) + $utils.throwErrByPath("visit.invalid_headers") + consoleProps = {} if options.log + message = url + + if options.method != 'GET' + message = "#{options.method} #{message}" + options._log = Cypress.log({ + message: message consoleProps: -> consoleProps }) @@ -598,11 +626,13 @@ module.exports = (Commands, Cypress, cy, state, config) -> if url isnt originalUrl consoleProps["Original Url"] = originalUrl - if options.log and redirects and redirects.length - indicateRedirects = -> - [originalUrl].concat(redirects).join(" -> ") + if options.log + message = options._log.get('message') + + if redirects and redirects.length + message = [message].concat(redirects).join(" -> ") - options._log.set({message: indicateRedirects()}) + options._log.set({message: message}) consoleProps["Resolved Url"] = url consoleProps["Redirects"] = redirects diff --git a/packages/driver/src/cy/timers.js b/packages/driver/src/cy/timers.js index b1228cbf1a45..ba2ad3ebc993 100644 --- a/packages/driver/src/cy/timers.js +++ b/packages/driver/src/cy/timers.js @@ -82,13 +82,7 @@ const create = () => { let timerId let [fnOrCode, delay, ...params] = args - const timerOverride = (timestamp) => { - // https://github.com/cypress-io/cypress/issues/2725 - // requestAnimationFrame yields a high res timestamp - if (arguments.length) { - params = [timestamp] - } - + const timerOverride = (...params) => { // if we're currently paused then we need // to enqueue this timer callback and invoke // it immediately once we're unpaused diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index ec5dbc9a63db..bac9ff5f11c5 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -845,7 +845,15 @@ module.exports = { missing_preset: "#{cmd('viewport')} could not find a preset for: '{{preset}}'. Available presets are: {{presets}}" visit: - invalid_1st_arg: "#{cmd('visit')} must be called with a string as its 1st argument" + invalid_1st_arg: "#{cmd('visit')} must be called with a URL or an options object containing a URL as its 1st argument" + invalid_method: "#{cmd('visit')} was called with an invalid method: '{{method}}'. Method can only be GET or POST." + invalid_headers: "#{cmd('visit')} requires the 'headers' option to be an object." + no_duplicate_url: """ + #{cmd('visit')} must be called with only one URL. You specified two URLs: + + URL from the `options` object: {{optionsUrl}} + URL from the `url` parameter: {{url}} + """ cannot_visit_2nd_domain: """ #{cmd('visit')} failed because you are attempting to visit a second unique domain. diff --git a/packages/driver/test/cypress/integration/commands/navigation_spec.coffee b/packages/driver/test/cypress/integration/commands/navigation_spec.coffee index 2137a06e8b47..2caa1850f14e 100644 --- a/packages/driver/test/cypress/integration/commands/navigation_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/navigation_spec.coffee @@ -580,6 +580,37 @@ describe "src/cy/commands/navigation", -> expect(win.bar).to.not.exist expect(onLoad).not.to.have.been.called + it "can send headers", -> + cy.visit({ + url: "http://localhost:3500/dump-headers", + headers: { + "x-foo-baz": "bar-quux" + } + }) + cy.contains('"x-foo-baz":"bar-quux"') + + describe "can send a POST request", -> + it "automatically urlencoded using an object body", -> + cy.visit("http://localhost:3500/post-only", { + method: "POST", + body: { + bar: "baz" + } + }) + cy.contains("it worked!").contains("{\"bar\":\"baz\"}") + + it "with any string body and headers", -> + cy.visit("http://localhost:3500/post-only", { + method: "POST", + headers: { + "content-type": "application/json" + } + body: JSON.stringify({ + bar: "baz" + }) + }) + cy.contains("it worked!").contains("{\"bar\":\"baz\"}") + describe "when origins don't match", -> beforeEach -> Cypress.emit("test:before:run", { id: 888 }) @@ -856,6 +887,16 @@ describe "src/cy/commands/navigation", -> "http://localhost:3500/foo -> 1 -> 2" ) + it "indicates POST in the message", -> + cy.visit("http://localhost:3500/post-only", { + method: "POST" + }).then -> + lastLog = @lastLog + + expect(lastLog.get("message")).to.eq( + "POST http://localhost:3500/post-only" + ) + it "displays note in consoleProps when visiting the same page with a hash", -> cy.visit("http://localhost:3500/fixtures/generic.html#foo") .visit("http://localhost:3500/fixtures/generic.html#foo") @@ -934,11 +975,40 @@ describe "src/cy/commands/navigation", -> it "throws when url isnt a string", (done) -> cy.on "fail", (err) -> - expect(err.message).to.eq "cy.visit() must be called with a string as its 1st argument" + expect(err.message).to.eq "cy.visit() must be called with a URL or an options object containing a URL as its 1st argument" done() cy.visit() + it "throws when url is specified twice", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.contain "cy.visit() must be called with only one URL. You specified two URLs" + done() + + cy.visit("http://foobarbaz", { + url: "http://foobarbaz" + }) + + it "throws when method is unsupported", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.contain "cy.visit() was called with an invalid method: 'FOO'" + done() + + cy.visit({ + url: "http://foobarbaz", + method: "FOO" + }) + + it "throws when headers is not an object", (done) -> + cy.on "fail", (err) -> + expect(err.message).to.contain "cy.visit() requires the 'headers' option to be an object" + done() + + cy.visit({ + url: "http://foobarbaz", + headers: "quux" + }) + it "throws when attempting to visit a 2nd domain on different port", (done) -> cy.on "fail", (err) => lastLog = @lastLog @@ -1259,7 +1329,7 @@ describe "src/cy/commands/navigation", -> ## https://github.com/cypress-io/cypress/issues/3101 [{ - contentType: 'application/json', + contentType: 'application/json', pathName: 'json-content-type' }, { contentType: 'text/html; charset=utf-8,text/html', diff --git a/packages/driver/test/cypress/integration/cy/timers_spec.js b/packages/driver/test/cypress/integration/cy/timers_spec.js index ad98993f790f..265e27d1a8d2 100644 --- a/packages/driver/test/cypress/integration/cy/timers_spec.js +++ b/packages/driver/test/cypress/integration/cy/timers_spec.js @@ -37,6 +37,23 @@ describe('driver/src/cy/timers', () => { }) }) + it('setTimeout can pass multiple parameters to the target function', () => { + cy + .log('setTimeout should call target with two parameters') + .window() + .then((win) => { + win.foo = null + win.setFoo = (bar, baz) => { + win.foo = bar + baz + } + + win.setTimeout(win.setFoo, 0, 'bar', 'baz') + + cy + .window().its('foo').should('eq', 'barbaz') + }) + }) + it('setInterval is called through', () => { cy .log('setInterval should be called') @@ -73,6 +90,26 @@ describe('driver/src/cy/timers', () => { }) }) + it('setInterval can pass multiple parameters to the target function', () => { + cy + .log('setInterval should call target with two parameters') + .window() + .then((win) => { + win.foo = null + win.setFoo = (bar, baz) => { + win.foo = bar + baz + } + + const id1 = win.setInterval(win.setFoo, 1, 'bar', 'baz') + + cy + .window().its('foo').should('eq', 'barbaz') + .then(() => { + win.clearInterval(id1) + }) + }) + }) + it('requestAnimationFrame is called through', () => { cy .log('requestAnimationFrame should be called') diff --git a/packages/driver/test/support/server.coffee b/packages/driver/test/support/server.coffee index 1fffcd127f84..f16ec81f5be4 100644 --- a/packages/driver/test/support/server.coffee +++ b/packages/driver/test/support/server.coffee @@ -72,6 +72,12 @@ niv.install("react-dom@15.6.1") res.setHeader('Content-Type', 'text/html; charset=utf-8,text/html') res.end("Test
Hello
") + app.post '/post-only', (req, res) -> + res.send("it worked!
request body:
#{JSON.stringify(req.body)}") + + app.get '/dump-headers', (req, res) -> + res.send("request headers:
#{JSON.stringify(req.headers)}") + app.get "/status-404", (req, res) -> res .status(404) diff --git a/packages/reporter/package.json b/packages/reporter/package.json index cf121b043273..5a56680789a3 100644 --- a/packages/reporter/package.json +++ b/packages/reporter/package.json @@ -33,7 +33,7 @@ "classnames": "2.2.6", "css-element-queries": "0.4.0", "enzyme": "3.9.0", - "enzyme-adapter-react-16": "1.9.1", + "enzyme-adapter-react-16": "1.10.0", "font-awesome": "4.7.0", "jsdom": "13.2.0", "lodash": "4.17.11", diff --git a/packages/runner/package.json b/packages/runner/package.json index 2ad2467da8f8..36e3934df572 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -32,7 +32,7 @@ "chai-enzyme": "1.0.0-beta.1", "classnames": "2.2.6", "enzyme": "3.9.0", - "enzyme-adapter-react-16": "1.9.1", + "enzyme-adapter-react-16": "1.10.0", "font-awesome": "4.7.0", "jsdom": "13.2.0", "lodash": "4.17.11", diff --git a/packages/server/README.md b/packages/server/README.md index 46346feea42d..7c0c79dc2831 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -70,4 +70,6 @@ To run an individual e2e test: ```bash ## runs tests that match "base_url" npm run test-e2e -- --spec base_url -``` \ No newline at end of file +``` + +To update snapshots, see `snap-shot-it` instructions: https://github.com/bahmutov/snap-shot-it#advanced-use \ No newline at end of file diff --git a/packages/server/__snapshots__/7_record_spec.coffee.js b/packages/server/__snapshots__/7_record_spec.coffee.js index 7447e4a7b8de..b0ad0f250a2c 100644 --- a/packages/server/__snapshots__/7_record_spec.coffee.js +++ b/packages/server/__snapshots__/7_record_spec.coffee.js @@ -592,13 +592,11 @@ https://on.cypress.io/recording-project-runs ` exports['e2e record api interaction errors recordKey and projectId errors and exits on 401 1'] = ` -We failed trying to authenticate this project: pid123 - -Your Record Key is invalid: f858a...ee7e1 +Your Record Key f858a...ee7e1 is not valid with this project: pid123 It may have been recently revoked by you or another user. -Please log into the Dashboard to see the updated token. +Please log into the Dashboard to see the valid record keys. https://on.cypress.io/dashboard/projects/pid123 diff --git a/packages/server/lib/errors.coffee b/packages/server/lib/errors.coffee index 3a4f522b86dc..7154218bf779 100644 --- a/packages/server/lib/errors.coffee +++ b/packages/server/lib/errors.coffee @@ -427,13 +427,11 @@ getMsgByType = (type, arg1 = {}, arg2) -> """ when "DASHBOARD_RECORD_KEY_NOT_VALID" """ - We failed trying to authenticate this project: #{chalk.blue(arg2)} - - Your Record Key is invalid: #{chalk.yellow(arg1)} + Your Record Key #{chalk.yellow(arg1)} is not valid with this project: #{chalk.blue(arg2)} It may have been recently revoked by you or another user. - Please log into the Dashboard to see the updated token. + Please log into the Dashboard to see the valid record keys. https://on.cypress.io/dashboard/projects/#{arg2} """ diff --git a/packages/server/lib/server.coffee b/packages/server/lib/server.coffee index 021f7df6a5ef..cff31142c941 100644 --- a/packages/server/lib/server.coffee +++ b/packages/server/lib/server.coffee @@ -458,15 +458,19 @@ class Server @_remoteDomainName = previousState.domainName @_remoteVisitingUrl = previousState.visiting - request.sendStream(headers, automationRequest, { + # if they're POSTing an object, querystringify their POST body + if options.method == 'POST' and _.isObject(options.body) + options.form = options.body + delete options.body + + _.assign(options, { ## turn off gzip since we need to eventually ## rewrite these contents - auth: options.auth gzip: false url: urlFile ? urlStr - headers: { + headers: _.assign({ accept: "text/html,*/*" - } + }, options.headers) followRedirect: (incomingRes) -> status = incomingRes.statusCode next = incomingRes.headers.location @@ -479,6 +483,10 @@ class Server return true }) + + debug('sending request with options %o', options) + + request.sendStream(headers, automationRequest, options) .then(handleReqStream) .catch(error)