packages feed

webdriver-0.13.0.0: src/Test/WebDriver/Util/Commands.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-deriving-typeable #-}

{-# OPTIONS_HADDOCK not-home #-}
-- | Internal functions used to implement the functions exported by
-- "Test.WebDriver.Commands". These may be useful for implementing non-standard
-- webdriver commands.
module Test.WebDriver.Util.Commands (
  -- * Low-level webdriver functions
  doCommand
  -- ** Commands with :sessionId URL parameter
  , doSessCommand
  , SessionId(..)
  -- ** Commands with element :id URL parameters
  , doElemCommand
  , Element(..)
  -- ** Commands with :windowHandle URL parameters
  , WindowHandle(..)

  -- * Helpers
  , urlEncode
  ) where

import Control.Applicative
import Control.Monad.IO.Class
import Data.Aeson
import Data.Aeson.TH
import Data.Aeson.Types
import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Lazy.Char8 (ByteString)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding as TE
import GHC.Stack
import Network.HTTP.Client (Response(..))
import Network.HTTP.Types.Header
import Network.HTTP.Types.Status (Status(..))
import qualified Network.HTTP.Types.URI as HTTP
import Prelude -- hides some "unused import" warnings
import Test.WebDriver.Capabilities.Aeson
import Test.WebDriver.Exceptions
import Test.WebDriver.JSON
import Test.WebDriver.Types
import Test.WebDriver.Util.Aeson
import UnliftIO.Exception


data SuccessResponse a = SuccessResponse {
  successValue :: a
  }
deriveFromJSON toCamel1 ''SuccessResponse

-- | An opaque identifier for a web page element
newtype Element = Element Text
  deriving (Eq, Ord, Show, Read)

instance FromJSON Element where
  parseJSON (Object o) = case fmap snd (aesonToList o) of
    (String id' : _) -> pure $ Element id'
    _ -> fail "No elements returned"
  parseJSON v = typeMismatch "Element" v

instance ToJSON Element where
  toJSON (Element e) = object ["element-6066-11e4-a52e-4f735466cecf" .= e]


-- | An opaque identifier for a browser window
newtype WindowHandle = WindowHandle Text
  deriving (Eq, Ord, Show, Read, FromJSON, ToJSON)

-- | This a convenient wrapper around 'doCommand' that automatically prepends
-- the session URL parameter to the wire command URL. For example, passing
-- a URL of \"/refresh\" will expand to \"/session/:sessionId/refresh\", where
-- :sessionId is a URL parameter as described in
-- <https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol>
doSessCommand :: (
  HasCallStack, WebDriver wd, ToJSON a, FromJSON b
  ) => Method -> Text -> a -> wd b
doSessCommand method path args = do
  Session { sessionId = SessionId sId } <- getSession
  -- Catch BadJSON exceptions here, since most commands go through this function.
  -- Then, re-throw them with "error", which automatically appends a callstack
  -- to the message in modern GHCs.
  -- This callstack makes it easy to see which command caused the BadJSON exception,
  -- without exposing too many internals.
  catch
    (doCommand method (T.concat ["/session/", urlEncode sId, path]) args)
    (\(e :: BadJSON) -> error $ show e)

-- | A wrapper around 'doSessCommand' to create element URLs.
-- For example, passing a URL of "/active" will expand to
-- \"/session/:sessionId/element/:id/active\", where :sessionId and :id are URL
-- parameters as described in the wire protocol.
doElemCommand :: (
  HasCallStack, WebDriver wd, ToJSON a, FromJSON b
  ) => Method -> Element -> Text -> a -> wd b
doElemCommand m (Element e) path a =
  doSessCommand m (T.concat ["/element/", urlEncode e, path]) a

urlEncode :: Text -> Text
urlEncode = TE.decodeUtf8 . HTTP.urlEncode False . TE.encodeUtf8

-- | Parse a 'FailedCommand' object from a given HTTP response.
getJSONResult :: (MonadIO m, FromJSON a) => Response ByteString -> m (Either SomeException a)
getJSONResult r
  | code >= 400 && code < 600 =
      case lookup hContentType headers of
        Just ct
          | "application/json" `BS.isInfixOf` ct -> do
              SuccessResponse {..} :: SuccessResponse FailedCommand <- parseJSON' body
              throwIO successValue
          | otherwise ->
              return $ Left $ toException $ ServerError reason
        Nothing ->
          return $ Left $ toException $ ServerError ("HTTP response missing content type. Server reason was: " <> reason)
  | code >= 200 && code < 300 = do
      SuccessResponse {successValue} <- parseJSON' body
      return $ Right successValue
  | otherwise = return $ Left $ toException $ (HTTPStatusUnknown code) reason
  where
    code = statusCode status
    reason = BS.unpack $ statusMessage status
    status = responseStatus r
    body = responseBody r
    headers = responseHeaders r


doCommand :: (
  HasCallStack, WebDriver m, ToJSON a, FromJSON b
  )
  -- | HTTP request method
  => Method
  -- | URL of request
  -> Text
  -- | JSON parameters passed in the body of the request. Note that, as a
  -- special case, anything that converts to Data.Aeson.Null will result in an
  -- empty request body.
  -> a
  -- | The JSON result of the HTTP request.
  -> m b
doCommand method url params = do
  Session {sessionDriver} <- getSession
  doCommandBase sessionDriver method url params
    >>= getJSONResult
    >>= either throwIO return