Skip to content

Commit

Permalink
Visit with a POST and/or custom headers (#3489)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
flotwig committed Feb 27, 2019
1 parent f70306a commit d24285b
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 12 deletions.
5 changes: 5 additions & 0 deletions cli/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
40 changes: 35 additions & 5 deletions packages/driver/src/cy/commands/navigation.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
})

Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/driver/src/cypress/error_messages.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 6 additions & 0 deletions packages/driver/test/support/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions packages/server/lib/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -479,6 +483,10 @@ class Server

return true
})

debug('sending request with options %o', options)

request.sendStream(headers, automationRequest, options)
.then(handleReqStream)
.catch(error)

Expand Down

0 comments on commit d24285b

Please sign in to comment.