Document much of yesod-test, especially the "Making Requests" section

This commit is contained in:
Maximilian Tagher 2015-01-25 15:20:25 -08:00
parent 3f20c759dc
commit cb4785cf28
2 changed files with 171 additions and 36 deletions

View File

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

View File

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