Visit with a POST and/or custom headers (#3489)

* driver, server: visit with a POST [wip]

* driver, server: allow sending body, headers, method in .visit

* driver: test: doublequotes

* driver: api cleanup, error handling

* driver, server: tests

* driver: only recognize visit(opts) if options is sole argument

* server: don't confuse options

* driver: validate method passed to 'visit'

* driver: validate that headers is an object

* driver: shows URL and not object in command log (fixes part of #678)

* cli: add new cy.visit(opts) invocation
This commit is contained in:
Zach Bloomquist
2019-02-27 09:19:31 -08:00
committed by GitHub
parent f70306a63c
commit d24285bbdd
6 changed files with 139 additions and 12 deletions

View File

@@ -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<VisitOptions>): Chainable<Window>
visit(options: Partial<VisitOptions> & { url: string }): Chainable<Window>
/**
* Wait for a number of milliseconds.

View File

@@ -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')
options._log.set({message: indicateRedirects()})
if redirects and redirects.length
message = [message].concat(redirects).join(" -> ")
options._log.set({message: message})
consoleProps["Resolved Url"] = url
consoleProps["Redirects"] = redirects

View File

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

View File

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

View File

@@ -72,6 +72,12 @@ niv.install("react-dom@15.6.1")
res.setHeader('Content-Type', 'text/html; charset=utf-8,text/html')
res.end("<html><head><title>Test</title></head><body><center>Hello</center></body></html>")
app.post '/post-only', (req, res) ->
res.send("<html><body>it worked!<br>request body:<br>#{JSON.stringify(req.body)}</body></html>")
app.get '/dump-headers', (req, res) ->
res.send("<html><body>request headers:<br>#{JSON.stringify(req.headers)}</body></html>")
app.get "/status-404", (req, res) ->
res
.status(404)

View File

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