Document much of yesod-test, especially the "Making Requests" section
This commit is contained in:
parent
3f20c759dc
commit
cb4785cf28
@ -10,7 +10,7 @@ using css selectors.
|
||||
|
||||
You can also easily build requests using forms present in the current page.
|
||||
This is very useful for testing web applications built in yesod for example,
|
||||
were your forms may have field names generated by the framework or a randomly
|
||||
where your forms may have field names generated by the framework or a randomly
|
||||
generated "\_token" field.
|
||||
|
||||
Your database is also directly available so you can use runDB to set up
|
||||
|
||||
@ -8,19 +8,19 @@
|
||||
Yesod.Test is a pragmatic framework for testing web applications built
|
||||
using wai and persistent.
|
||||
|
||||
By pragmatic I may also mean 'dirty'. It's main goal is to encourage integration
|
||||
By pragmatic I may also mean 'dirty'. Its main goal is to encourage integration
|
||||
and system testing of web applications by making everything /easy to test/.
|
||||
|
||||
Your tests are like browser sessions that keep track of cookies and the last
|
||||
visited page. You can perform assertions on the content of HTML responses,
|
||||
using css selectors to explore the document more easily.
|
||||
using CSS selectors to explore the document more easily.
|
||||
|
||||
You can also easily build requests using forms present in the current page.
|
||||
This is very useful for testing web applications built in yesod for example,
|
||||
were your forms may have field names generated by the framework or a randomly
|
||||
generated '_nonce' field.
|
||||
This is very useful for testing web applications built in yesod, for example,
|
||||
where your forms may have field names generated by the framework or a randomly
|
||||
generated nonce value.
|
||||
|
||||
Your database is also directly available so you can use runDBRunner to set up
|
||||
Your database is also directly available so you can use 'runDB' to set up
|
||||
backend pre-conditions, or to assert that your session is having the desired effect.
|
||||
|
||||
-}
|
||||
@ -38,12 +38,12 @@ module Yesod.Test
|
||||
, yit
|
||||
|
||||
-- * Making requests
|
||||
-- | To make a request you need to point to an url and pass in some parameters.
|
||||
--
|
||||
-- To build your parameters you will use the RequestBuilder monad that lets you
|
||||
-- add values, add files, lookup fields by label and find the current
|
||||
-- nonce value and add it to your request too.
|
||||
-- | You can construct requests with the 'RequestBuilder' monad, which lets you
|
||||
-- set the URL and add parameters, headers, and files. Helper functions are provided to
|
||||
-- lookup fields by label and to add the current nonce value from your forms.
|
||||
-- Once built, the request can be executed with the 'request' method.
|
||||
--
|
||||
-- Convenience functions like 'get' and 'post' build and execute common requests.
|
||||
, get
|
||||
, post
|
||||
, postBody
|
||||
@ -57,16 +57,22 @@ module Yesod.Test
|
||||
, RequestBuilder
|
||||
, setUrl
|
||||
|
||||
-- | Yesod can auto generate field ids, so you are never sure what
|
||||
-- the argument name should be for each one of your args when constructing
|
||||
-- *** Adding fields by label
|
||||
-- | Yesod can auto generate field names, so you are never sure what
|
||||
-- the argument name should be for each one of your inputs when constructing
|
||||
-- your requests. What you do know is the /label/ of the field.
|
||||
-- These functions let you add parameters to your request based
|
||||
-- on currently displayed label names.
|
||||
, byLabel
|
||||
, fileByLabel
|
||||
|
||||
-- | Does the current form have a _nonce? Use any of these to add it to your
|
||||
-- request parameters.
|
||||
-- *** Nonces
|
||||
-- | In order to prevent CSRF exploits, yesod-form adds a hidden input
|
||||
-- to your forms with the name "_token". This token is a randomly generated,
|
||||
-- per-session value called a /nonce/.
|
||||
--
|
||||
-- In order to prevent your forms from being rejected in tests, use one of
|
||||
-- these functions to add the nonce to your request.
|
||||
, addNonce
|
||||
, addNonce_
|
||||
|
||||
@ -188,7 +194,7 @@ data RequestPart
|
||||
= ReqKvPart T.Text T.Text
|
||||
| ReqFilePart T.Text FilePath BSL8.ByteString T.Text
|
||||
|
||||
-- | The RequestBuilder state monad constructs an url encoded string of arguments
|
||||
-- | The 'RequestBuilder' state monad constructs a URL encoded string of arguments
|
||||
-- to send with your requests. Some of the functions that run on it use the current
|
||||
-- response to analyze the forms that the server is expecting to receive.
|
||||
type RequestBuilder site = ST.StateT (RequestBuilderData site) IO
|
||||
@ -274,12 +280,12 @@ withResponse' getter f = maybe err f . getter =<< ST.get
|
||||
withResponse :: (SResponse -> YesodExample site a) -> YesodExample site a
|
||||
withResponse = withResponse' yedResponse
|
||||
|
||||
-- | Use HXT to parse a value from an html tag.
|
||||
-- | Use HXT to parse a value from an HTML tag.
|
||||
-- Check for usage examples in this module's source.
|
||||
parseHTML :: HtmlLBS -> Cursor
|
||||
parseHTML html = fromDocument $ HD.parseLBS html
|
||||
|
||||
-- | Query the last response using css selectors, returns a list of matched fragments
|
||||
-- | Query the last response using CSS selectors, returns a list of matched fragments
|
||||
htmlQuery' :: MonadIO m
|
||||
=> (state -> Maybe SResponse)
|
||||
-> Query
|
||||
@ -289,7 +295,7 @@ htmlQuery' getter query = withResponse' getter $ \ res ->
|
||||
Left err -> failure $ query <> " did not parse: " <> T.pack (show err)
|
||||
Right matches -> return $ map (encodeUtf8 . TL.pack) matches
|
||||
|
||||
-- | Query the last response using css selectors, returns a list of matched fragments
|
||||
-- | Query the last response using CSS selectors, returns a list of matched fragments
|
||||
htmlQuery :: Query -> YesodExample site [HtmlLBS]
|
||||
htmlQuery = htmlQuery' yedResponse
|
||||
|
||||
@ -354,7 +360,7 @@ bodyContains text = withResponse $ \ res ->
|
||||
contains :: BSL8.ByteString -> String -> Bool
|
||||
contains a b = DL.isInfixOf b (TL.unpack $ decodeUtf8 a)
|
||||
|
||||
-- | Queries the html using a css selector, and all matched elements must contain
|
||||
-- | Queries the HTML using a CSS selector, and all matched elements must contain
|
||||
-- the given string.
|
||||
htmlAllContain :: Query -> String -> YesodExample site ()
|
||||
htmlAllContain query search = do
|
||||
@ -364,7 +370,7 @@ htmlAllContain query search = do
|
||||
_ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $
|
||||
DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches)
|
||||
|
||||
-- | Queries the html using a css selector, and passes if any matched
|
||||
-- | Queries the HTML using a CSS selector, and passes if any matched
|
||||
-- element contains the given string.
|
||||
--
|
||||
-- Since 0.3.5
|
||||
@ -376,7 +382,7 @@ htmlAnyContain query search = do
|
||||
_ -> liftIO $ HUnit.assertBool ("None of "++T.unpack query++" contain "++search) $
|
||||
DL.any (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches)
|
||||
|
||||
-- | Queries the html using a css selector, and fails if any matched
|
||||
-- | Queries the HTML using a CSS selector, and fails if any matched
|
||||
-- element contains the given string (in other words, it is the logical
|
||||
-- inverse of htmlAnyContains).
|
||||
--
|
||||
@ -389,7 +395,7 @@ htmlNoneContain query search = do
|
||||
found -> failure $ "Found " <> T.pack (show $ length found) <>
|
||||
" instances of " <> T.pack search <> " in " <> query <> " elements"
|
||||
|
||||
-- | Performs a css query on the last response and asserts the matched elements
|
||||
-- | Performs a CSS query on the last response and asserts the matched elements
|
||||
-- are as many as expected.
|
||||
htmlCount :: Query -> Int -> YesodExample site ()
|
||||
htmlCount query count = do
|
||||
@ -408,7 +414,7 @@ printMatches query = do
|
||||
matches <- htmlQuery query
|
||||
liftIO $ hPutStrLn stderr $ show matches
|
||||
|
||||
-- | Add a parameter with the given name and value.
|
||||
-- | Add a parameter with the given name and value to the request body.
|
||||
addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
|
||||
addPostParam name value =
|
||||
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
|
||||
@ -416,16 +422,25 @@ addPostParam name value =
|
||||
addPostData (MultipleItemsPostData posts) =
|
||||
MultipleItemsPostData $ ReqKvPart name value : posts
|
||||
|
||||
-- | Add a parameter with the given name and value to the query string.
|
||||
addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
|
||||
addGetParam name value = ST.modify $ \rbd -> rbd
|
||||
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
|
||||
: rbdGets rbd
|
||||
}
|
||||
|
||||
-- | Add a file to be posted with the current request
|
||||
-- | Add a file to be posted with the current request.
|
||||
--
|
||||
-- Adding a file will automatically change your request content-type to be multipart/form-data
|
||||
addFile :: T.Text -> FilePath -> T.Text -> RequestBuilder site ()
|
||||
-- Adding a file will automatically change your request content-type to be multipart/form-data.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > addFile "profile_picture" "static/img/picture.png" "img/png"
|
||||
addFile :: T.Text -- ^ The parameter name for the file.
|
||||
-> FilePath -- ^ The path to the file.
|
||||
-> T.Text -- ^ The MIME type of the file, e.g. "image/png".
|
||||
-> RequestBuilder site ()
|
||||
addFile name path mimetype = do
|
||||
contents <- liftIO $ BSL8.readFile path
|
||||
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) }
|
||||
@ -476,18 +491,75 @@ nameFromLabel label = do
|
||||
(<>) :: T.Text -> T.Text -> T.Text
|
||||
(<>) = T.append
|
||||
|
||||
byLabel :: T.Text -> T.Text -> RequestBuilder site ()
|
||||
-- How does this work for the alternate <label><input></label> syntax?
|
||||
|
||||
-- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a parameter
|
||||
-- for that input to the request body.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- Given this HTML, we want to submit @f1=Michael@ to the server:
|
||||
--
|
||||
-- > <form method="POST">
|
||||
-- > <label for="user">Username</label>
|
||||
-- > <input id="user" name="f1" />
|
||||
-- > </form>
|
||||
--
|
||||
-- You can set this parameter like so:
|
||||
--
|
||||
-- > request $ do
|
||||
-- > byLabel "Username" "Michael"
|
||||
--
|
||||
-- This function also supports the implicit label syntax, in which
|
||||
-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:
|
||||
--
|
||||
-- > <form method="POST">
|
||||
-- > <label>Username <input name="f1"> </label>
|
||||
-- > </form>
|
||||
byLabel :: T.Text -- ^ The text contained in the @\<label>@.
|
||||
-> T.Text -- ^ The value to set the parameter to.
|
||||
-> RequestBuilder site ()
|
||||
byLabel label value = do
|
||||
name <- nameFromLabel label
|
||||
addPostParam name value
|
||||
|
||||
fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder site ()
|
||||
-- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a file for that input to the request body.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- Given this HTML, we want to submit a file with the parameter name @f1@ to the server:
|
||||
--
|
||||
-- > <form method="POST">
|
||||
-- > <label for="imageInput">Please submit an image</label>
|
||||
-- > <input id="imageInput" type="file" name="f1" accept="image/*">
|
||||
-- > </form>
|
||||
--
|
||||
-- You can set this parameter like so:
|
||||
--
|
||||
-- > request $ do
|
||||
-- > fileByLabel "Please submit an image" "static/img/picture.png" "img/png"
|
||||
--
|
||||
-- This function also supports the implicit label syntax, in which
|
||||
-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:
|
||||
--
|
||||
-- > <form method="POST">
|
||||
-- > <label>Please submit an image <input type="file" name="f1"> </label>
|
||||
-- > </form>
|
||||
fileByLabel :: T.Text -- ^ The text contained in the @\<label>@.
|
||||
-> FilePath -- ^ The path to the file.
|
||||
-> T.Text -- ^ The MIME type of the file, e.g. "image/png".
|
||||
-> RequestBuilder site ()
|
||||
fileByLabel label path mime = do
|
||||
name <- nameFromLabel label
|
||||
addFile name path mime
|
||||
|
||||
-- | Lookup a _nonce form field and add it's value to the params.
|
||||
-- | Lookup a _token form field and add its value to the params.
|
||||
-- Receives a CSS selector that should resolve to the form element containing the nonce.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > addNonce_ "#formID"
|
||||
addNonce_ :: Query -> RequestBuilder site ()
|
||||
addNonce_ scope = do
|
||||
matches <- htmlQuery' rbdResponse $ scope <> "input[name=_token][type=hidden][value]"
|
||||
@ -500,7 +572,11 @@ addNonce_ scope = do
|
||||
addNonce :: RequestBuilder site ()
|
||||
addNonce = addNonce_ ""
|
||||
|
||||
-- | Perform a POST request to url
|
||||
-- | Perform a POST request to @url@.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > post HomeR
|
||||
post :: (Yesod site, RedirectUrl site url)
|
||||
=> url
|
||||
-> YesodExample site ()
|
||||
@ -508,7 +584,14 @@ post url = request $ do
|
||||
setMethod "POST"
|
||||
setUrl url
|
||||
|
||||
-- | Perform a POST request to url with sending a body into it.
|
||||
-- | Perform a POST request to @url@ with the given body.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > postBody HomeR "foobar"
|
||||
--
|
||||
-- > import Data.Aeson
|
||||
-- > postBody HomeR (encode $ object ["age" .= (1 :: Integer)])
|
||||
postBody :: (Yesod site, RedirectUrl site url)
|
||||
=> url
|
||||
-> BSL8.ByteString
|
||||
@ -518,7 +601,13 @@ postBody url body = request $ do
|
||||
setUrl url
|
||||
setRequestBody body
|
||||
|
||||
-- | Perform a GET request to url, using params
|
||||
-- | Perform a GET request to @url@.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > get HomeR
|
||||
--
|
||||
-- > get ("http://google.com" :: Text)
|
||||
get :: (Yesod site, RedirectUrl site url)
|
||||
=> url
|
||||
-> YesodExample site ()
|
||||
@ -526,9 +615,28 @@ get url = request $ do
|
||||
setMethod "GET"
|
||||
setUrl url
|
||||
|
||||
-- | Sets the HTTP method used by the request.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > setMethod "POST"
|
||||
--
|
||||
-- > import Network.HTTP.Types.Method
|
||||
-- > request $ do
|
||||
-- > setMethod methodPut
|
||||
setMethod :: H.Method -> RequestBuilder site ()
|
||||
setMethod m = ST.modify $ \rbd -> rbd { rbdMethod = m }
|
||||
|
||||
-- | Sets the URL used by the request.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > setUrl HomeR
|
||||
--
|
||||
-- > request $ do
|
||||
-- > setUrl ("http://google.com/" :: Text)
|
||||
setUrl :: (Yesod site, RedirectUrl site url)
|
||||
=> url
|
||||
-> RequestBuilder site ()
|
||||
@ -551,18 +659,45 @@ setUrl url' = do
|
||||
}
|
||||
|
||||
-- | Simple way to set HTTP request body
|
||||
--
|
||||
-- ==== __ Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > setRequestBody "foobar"
|
||||
--
|
||||
-- > import Data.Aeson
|
||||
-- > request $ do
|
||||
-- > setRequestBody $ encode $ object ["age" .= (1 :: Integer)]
|
||||
setRequestBody :: (Yesod site)
|
||||
=> BSL8.ByteString
|
||||
-> RequestBuilder site ()
|
||||
setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = BinaryPostData body }
|
||||
|
||||
-- | Adds the given header to the request; see "Network.HTTP.Types.Header" for creating 'Header's.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > import Network.HTTP.Types.Header
|
||||
-- > request $ do
|
||||
-- > addRequestHeader (hUserAgent, "Chrome/41.0.2228.0")
|
||||
addRequestHeader :: H.Header -> RequestBuilder site ()
|
||||
addRequestHeader header = ST.modify $ \rbd -> rbd
|
||||
{ rbdHeaders = header : rbdHeaders rbd
|
||||
}
|
||||
|
||||
-- | General interface to performing requests, allowing you to add extra
|
||||
-- headers as well as letting you specify the request method.
|
||||
-- | The general interface for performing requests. 'request' takes a 'RequestBuilder',
|
||||
-- constructs a request, and executes it.
|
||||
--
|
||||
-- The 'RequestBuilder' allows you to build up attributes of the request, like the
|
||||
-- headers, parameters, and URL of the request.
|
||||
--
|
||||
-- ==== __Examples__
|
||||
--
|
||||
-- > request $ do
|
||||
-- > addNonce
|
||||
-- > byLabel "First Name" "Felipe"
|
||||
-- > setMethod "PUT"
|
||||
-- > setUrl NameR
|
||||
request :: Yesod site
|
||||
=> RequestBuilder site ()
|
||||
-> YesodExample site ()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user