diff --git a/package.yaml b/package.yaml index c434d0d..893d3b6 100644 --- a/package.yaml +++ b/package.yaml @@ -72,6 +72,7 @@ library: - zip-archive >= 0.1.1.8 exposed-modules: - Test.WebDriver + - Test.WebDriver.Actions.Internal - Test.WebDriver.Capabilities - Test.WebDriver.Chrome.Extension - Test.WebDriver.Class @@ -87,6 +88,7 @@ library: - Test.WebDriver.Firefox.Profile - Test.WebDriver.Internal - Test.WebDriver.JSON + - Test.WebDriver.JSON.Internal - Test.WebDriver.Monad - Test.WebDriver.Session - Test.WebDriver.Session.History diff --git a/src/Test/WebDriver/Actions/Internal.hs b/src/Test/WebDriver/Actions/Internal.hs new file mode 100644 index 0000000..c75a20f --- /dev/null +++ b/src/Test/WebDriver/Actions/Internal.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE OverloadedStrings #-} + +module Test.WebDriver.Actions.Internal where + +import Data.Aeson +import Data.Aeson.TH +import Data.Text (Text) +import Test.WebDriver.Commands.Internal (Element) +import Test.WebDriver.JSON.Internal (lower1) + +data Actions = Actions + { actionsId :: Text + , actionsType :: ActionsType + , actionsParameters :: Maybe ActionsParameters + , actionsActions :: [Action] + } deriving (Eq, Show) + +data ActionsType = + ActionsPointer + | ActionsKey + | ActionsNone + deriving (Eq, Show) + +data ActionsParameters = ActionsParameters + { paramsPointerType :: Maybe PointerType + } deriving (Eq, Show) + +data Action = Action + { actionType :: ActionType + , actionDurtion :: Maybe Int + , actionX :: Maybe Int + , actionY :: Maybe Int + , actionOrigin :: Maybe MoveOrigin + , actionValue :: Maybe Text + , actionButton :: Maybe MouseButton + } deriving (Eq, Show) + +data PointerType = + PointerMouse + | PointerPen + | PointerTouch + deriving (Eq, Show) + +data MoveOrigin = + OriginViewport + | OriginPointer + | OriginElement Element + deriving (Eq, Show) + +instance ToJSON MoveOrigin where + toJSON OriginViewport = String "viewport" + toJSON OriginPointer = String "pointer" + toJSON (OriginElement e) = toJSON e + +data ActionType = + ActionPause + | ActionKeyUp + | ActionKeyDown + | ActionPointerUp + | ActionPointerDown + | ActionPointerMove + | ActionPointerCancel + | ActionScroll + deriving (Eq, Show) + +-- |A mouse button +data MouseButton = + LeftButton + | MiddleButton + | RightButton + deriving (Eq, Show, Ord, Bounded, Enum) + +instance ToJSON MouseButton where + toJSON = toJSON . fromEnum + +instance FromJSON MouseButton where + parseJSON v = do + n <- parseJSON v + case n :: Integer of + 0 -> return LeftButton + 1 -> return MiddleButton + 2 -> return RightButton + err -> fail $ "Invalid JSON for MouseButton: " ++ show err + +$(deriveToJSON (defaultOptions{constructorTagModifier = lower1 . drop 7}) ''PointerType) +$(deriveToJSON (defaultOptions{constructorTagModifier = lower1 . drop 6}) ''ActionType) +$(deriveToJSON (defaultOptions{constructorTagModifier = lower1 . drop 7}) ''ActionsType) +$(deriveToJSON (defaultOptions{fieldLabelModifier = lower1 . drop 6, omitNothingFields = True}) ''Action) +$(deriveToJSON (defaultOptions{fieldLabelModifier = lower1 . drop 6, omitNothingFields = True}) ''ActionsParameters) +$(deriveToJSON (defaultOptions{fieldLabelModifier = lower1 . drop 7, omitNothingFields = True}) ''Actions) + +pointerMoveAction :: (Int, Int) -> MoveOrigin -> Action +pointerMoveAction (x, y) origin = + Action + { actionType = ActionPointerMove + , actionDurtion = Nothing + , actionX = Just x + , actionY = Just y + , actionOrigin = Just origin + , actionValue = Nothing + , actionButton = Nothing + } + +pointerDownAction :: Action +pointerDownAction = + Action + { actionType = ActionPointerDown + , actionDurtion = Nothing + , actionX = Nothing + , actionY = Nothing + , actionOrigin = Nothing + , actionValue = Nothing + , actionButton = Nothing + } + +pointerUpAction :: Action +pointerUpAction = + Action + { actionType = ActionPointerUp + , actionDurtion = Nothing + , actionX = Nothing + , actionY = Nothing + , actionOrigin = Nothing + , actionValue = Nothing + , actionButton = Nothing + } + +keyDownAction :: Text -> Action +keyDownAction c = + Action + { actionType = ActionKeyDown + , actionDurtion = Nothing + , actionX = Nothing + , actionY = Nothing + , actionOrigin = Nothing + , actionValue = Just c + , actionButton = Nothing + } + +keyUpAction :: Text -> Action +keyUpAction c = + Action + { actionType = ActionKeyUp + , actionDurtion = Nothing + , actionX = Nothing + , actionY = Nothing + , actionOrigin = Nothing + , actionValue = Just c + , actionButton = Nothing + } diff --git a/src/Test/WebDriver/Capabilities.hs b/src/Test/WebDriver/Capabilities.hs index 6171134..d2a4ca4 100644 --- a/src/Test/WebDriver/Capabilities.hs +++ b/src/Test/WebDriver/Capabilities.hs @@ -10,7 +10,7 @@ import Test.WebDriver.JSON import Data.Aeson import Data.Aeson.Types (Parser, typeMismatch, Pair) -import Data.Text (Text, toLower, toUpper) +import Data.Text (Text, toLower) import Data.Default.Class (Default(..)) import Data.Word (Word16) import Data.Maybe (fromMaybe, catMaybes) @@ -176,7 +176,7 @@ instance ToJSON Capabilities where object $ filter (\p -> snd p /= Null) $ [ "browserName" .= browser , "version" .= version - , "platform" .= platform + , "platformName" .= platform , "proxy" .= proxy , "javascriptEnabled" .= javascriptEnabled , "takesScreenshot" .= takesScreenshot @@ -204,7 +204,7 @@ instance ToJSON Capabilities where ] Chrome {..} -> catMaybes [ opt "chrome.chromedriverVersion" chromeDriverVersion ] - ++ [ "chromeOptions" .= object (catMaybes + ++ [ "goog:chromeOptions" .= object (catMaybes [ opt "binary" chromeBinary ] ++ [ "args" .= chromeOptions @@ -264,12 +264,7 @@ instance FromJSON Capabilities where browser <- req "browserName" Capabilities <$> getBrowserCaps browser <*> opt "version" Nothing - <*> do - p <- o .:? "platform" - pN <- o .:? "platformName" - case p <|> pN of - Just p' -> return p' - Nothing -> throw . BadJSON $ "platform or platformName required" + <*> opt "platformName" Any <*> opt "proxy" NoProxy <*> b "javascriptEnabled" <*> b "takesScreenshot" @@ -615,7 +610,8 @@ data Platform = Windows | XP | Vista | Mac | Linux | Unix | Any deriving (Eq, Show, Ord, Bounded, Enum) instance ToJSON Platform where - toJSON = String . toUpper . fromString . show + toJSON Any = Null + toJSON p = String . toLower . fromString $ show p instance FromJSON Platform where parseJSON (String jStr) = case toLower jStr of @@ -664,17 +660,17 @@ instance FromJSON ProxyType where instance ToJSON ProxyType where toJSON pt = object $ case pt of NoProxy -> - ["proxyType" .= ("DIRECT" :: String)] + ["proxyType" .= ("direct" :: String)] UseSystemSettings -> - ["proxyType" .= ("SYSTEM" :: String)] + ["proxyType" .= ("system" :: String)] AutoDetect -> - ["proxyType" .= ("AUTODETECT" :: String)] + ["proxyType" .= ("autodetect" :: String)] PAC{autoConfigUrl = url} -> - ["proxyType" .= ("PAC" :: String) + ["proxyType" .= ("pac" :: String) ,"proxyAutoconfigUrl" .= url ] Manual{ftpProxy = ftp, sslProxy = ssl, httpProxy = http} -> - ["proxyType" .= ("MANUAL" :: String) + ["proxyType" .= ("manual" :: String) ,"ftpProxy" .= ftp ,"sslProxy" .= ssl ,"httpProxy" .= http diff --git a/src/Test/WebDriver/Commands.hs b/src/Test/WebDriver/Commands.hs index dbb2a11..3982250 100644 --- a/src/Test/WebDriver/Commands.hs +++ b/src/Test/WebDriver/Commands.hs @@ -21,18 +21,18 @@ module Test.WebDriver.Commands -- *** Sending key inputs to elements , sendKeys, sendRawKeys, clearInput -- ** Element information - , attr, cssProp, elemPos, elemSize + , ElemRect(..) + , attr, prop, cssProp + , elemPos, elemSize, elemRect , isSelected, isEnabled, isDisplayed - , tagName, activeElem, elemInfo - -- ** Element equality - , (<==>), () + , tagName, activeElem -- * Javascript , executeJS, asyncJS , JSArg(..) -- * Windows - , WindowHandle(..), currentWindow + , WindowHandle(..), WindowRect(..) , getCurrentWindow, closeWindow, windows, focusWindow, maximize - , getWindowSize, setWindowSize, getWindowPos, setWindowPos + , windowRect, getWindowSize, setWindowSize, getWindowPos, setWindowPos -- * Focusing on frames , focusFrame, FrameSelector(..) -- * Cookies @@ -93,9 +93,11 @@ import qualified Data.Text as T import qualified Data.Text.Lazy.Encoding as TL import Data.Word import Network.URI hiding (path) -- suppresses warnings +import Test.WebDriver.Actions.Internal import Test.WebDriver.Capabilities import Test.WebDriver.Class import Test.WebDriver.Commands.Internal +import qualified Test.WebDriver.Common.Keys as Keys import Test.WebDriver.Cookies import Test.WebDriver.Exceptions.Internal import Test.WebDriver.JSON @@ -109,9 +111,15 @@ import Prelude -- hides some "unused import" warnings -- Note: if you're using 'runSession' to run your WebDriver commands, you don't need to call this explicitly. createSession :: (HasCallStack, WebDriver wd) => Capabilities -> wd WDSession createSession caps = do - let connect = withAuthHeaders $ doCommand methodPost "/session" . single "desiredCapabilities" $ caps + let connect = withAuthHeaders $ doCommand methodPost "/session" . single "capabilities" $ single "alwaysMatch" caps resp <- connect `L.catch` \(_ex :: FailedCommand) -> connect - modifySession $ \s -> s { wdSessCreateResponse = Just resp } + s <- getSession + let sessCpas = parseMaybe (.: "capabilities") resp + let sessId = parseMaybe (.: "sessionId") resp + putSession s + { wdSessCreateResponse = sessCpas + , wdSessId = SessionId <$> sessId + } getSession -- |Retrieve a list of active sessions and their 'Capabilities'. @@ -129,11 +137,11 @@ getSessionCaps :: (HasCallStack, WDSessionState s) => s (Maybe Capabilities) getSessionCaps = do caps <- wdSessCreateResponse <$> getSession return $ parseMaybe parseJSON =<< caps - + -- |Close the current session and the browser associated with it. closeSession :: (HasCallStack, WebDriver wd) => wd () closeSession = do s@WDSession {} <- getSession - noReturn $ doSessCommand methodDelete "" Null + noReturn $ doSessCommand methodDelete "" emptyObject putSession s { wdSessId = Nothing } @@ -174,15 +182,15 @@ openPage url -- |Navigate forward in the browser history. forward :: (HasCallStack, WebDriver wd) => wd () -forward = noReturn $ doSessCommand methodPost "/forward" Null +forward = noReturn $ doSessCommand methodPost "/forward" emptyObject -- |Navigate backward in the browser history. back :: (HasCallStack, WebDriver wd) => wd () -back = noReturn $ doSessCommand methodPost "/back" Null +back = noReturn $ doSessCommand methodPost "/back" emptyObject -- |Refresh the current page refresh :: (HasCallStack, WebDriver wd) => wd () -refresh = noReturn $ doSessCommand methodPost "/refresh" Null +refresh = noReturn $ doSessCommand methodPost "/refresh" emptyObject -- |An existential wrapper for any 'ToJSON' instance. This allows us to pass -- parameters of many different types to Javascript code. @@ -236,7 +244,7 @@ a `fromJSON` instance to use. executeJS :: (HasCallStack, F.Foldable f, FromJSON a, WebDriver wd) => f JSArg -> Text -> wd a executeJS a s = fromJSON' =<< getResult where - getResult = doSessCommand methodPost "/execute" . pair ("args", "script") $ (F.toList a,s) + getResult = doSessCommand methodPost "/execute/sync" . pair ("args", "script") $ (F.toList a,s) {- |Executes a snippet of Javascript code asynchronously. This function works similarly to 'executeJS', except that the Javascript is passed a callback @@ -248,7 +256,7 @@ Javascript function timed out (see 'setScriptTimeout') asyncJS :: (HasCallStack, F.Foldable f, FromJSON a, WebDriver wd) => f JSArg -> Text -> wd (Maybe a) asyncJS a s = handle timeout $ Just <$> (fromJSON' =<< getResult) where - getResult = doSessCommand methodPost "/execute_async" . pair ("args", "script") + getResult = doSessCommand methodPost "/execute/async" . pair ("args", "script") $ (F.toList a,s) timeout (FailedCommand Timeout _) = return Nothing timeout (FailedCommand ScriptTimeout _) = return Nothing @@ -279,7 +287,7 @@ activateIME :: (HasCallStack, WebDriver wd) => Text -> wd () activateIME = noReturn . doSessCommand methodPost "/ime/activate" . single "engine" deactivateIME :: (HasCallStack, WebDriver wd) => wd () -deactivateIME = noReturn $ doSessCommand methodPost "/ime/deactivate" Null +deactivateIME = noReturn $ doSessCommand methodPost "/ime/deactivate" emptyObject -- |Specifies the frame used by 'Test.WebDriver.Commands.focusFrame' @@ -306,45 +314,76 @@ focusFrame s = noReturn $ doSessCommand methodPost "/frame" . single "id" $ s -- |Returns a handle to the currently focused window getCurrentWindow :: (HasCallStack, WebDriver wd) => wd WindowHandle -getCurrentWindow = doSessCommand methodGet "/window_handle" Null +getCurrentWindow = doSessCommand methodGet "/window" Null -- |Returns a list of all windows available to the session windows :: (HasCallStack, WebDriver wd) => wd [WindowHandle] -windows = doSessCommand methodGet "/window_handles" Null +windows = doSessCommand methodGet "/window/handles" Null focusWindow :: (HasCallStack, WebDriver wd) => WindowHandle -> wd () -focusWindow w = noReturn $ doSessCommand methodPost "/window" . single "name" $ w +focusWindow w = noReturn $ doSessCommand methodPost "/window" . single "handle" $ w -- |Closes the given window closeWindow :: (HasCallStack, WebDriver wd) => WindowHandle -> wd () closeWindow w = do cw <- getCurrentWindow focusWindow w - ignoreReturn $ doSessCommand methodDelete "/window" Null + ignoreReturn $ doSessCommand methodDelete "/window" emptyObject unless (w == cw) $ focusWindow cw -- |Maximizes the current window if not already maximized maximize :: (HasCallStack, WebDriver wd) => wd () -maximize = ignoreReturn $ doWinCommand methodPost currentWindow "/maximize" Null +maximize = ignoreReturn $ doSessCommand methodPost "/window/maximize" emptyObject + +data WindowRect = WindowRect + { wrectX :: Int + , wrectY :: Int + , wrectWidth :: Word + , wrectHeight :: Word + } deriving (Eq, Show) + +instance FromJSON WindowRect where + parseJSON = withObject "WindowRect" $ \o -> do + wrectX <- o .: "x" + wrectY <- o .: "y" + wrectWidth <- o .: "width" + wrectHeight <- o .: "height" + pure $ WindowRect {..} + +-- |Retrieve current window's rect. +windowRect :: (HasCallStack, WebDriver wd) => wd WindowRect +windowRect = doSessCommand methodGet "/window/rect" Null + +-- |Set current window's rect. +setWindowRect :: (HasCallStack, WebDriver wd) => Maybe (Int, Int) -> Maybe (Word, Word) -> wd WindowRect +setWindowRect xy wh = do + doSessCommand methodPost "/window/rect" $ + object + [ "x" .= fmap fst xy + , "y" .= fmap snd xy + , "width" .= fmap fst wh + , "height" .= fmap snd wh + ] -- |Get the dimensions of the current window. getWindowSize :: (HasCallStack, WebDriver wd) => wd (Word, Word) -getWindowSize = doWinCommand methodGet currentWindow "/size" Null - >>= parsePair "width" "height" "getWindowSize" +getWindowSize = do + WindowRect {..} <- windowRect + pure (wrectWidth, wrectHeight) -- |Set the dimensions of the current window. setWindowSize :: (HasCallStack, WebDriver wd) => (Word, Word) -> wd () -setWindowSize = ignoreReturn . doWinCommand methodPost currentWindow "/size" - . pair ("width", "height") +setWindowSize wh = void $ setWindowRect Nothing (Just wh) -- |Get the coordinates of the current window. getWindowPos :: (HasCallStack, WebDriver wd) => wd (Int, Int) -getWindowPos = doWinCommand methodGet currentWindow "/position" Null - >>= parsePair "x" "y" "getWindowPos" +getWindowPos = do + WindowRect {..} <- windowRect + pure (wrectX, wrectY) -- |Set the coordinates of the current window. setWindowPos :: (HasCallStack, WebDriver wd) => (Int, Int) -> wd () -setWindowPos = ignoreReturn . doWinCommand methodPost currentWindow "/position" . pair ("x","y") +setWindowPos xy = void $ setWindowRect (Just xy) Nothing -- |Retrieve all cookies visible to the current page. cookies :: (HasCallStack, WebDriver wd) => wd [Cookie] @@ -359,14 +398,14 @@ setCookie = noReturn . doSessCommand methodPost "/cookie" . single "cookie" -- |Delete a cookie. This will do nothing is the cookie isn't visible to the -- current page. deleteCookie :: (HasCallStack, WebDriver wd) => Cookie -> wd () -deleteCookie c = noReturn $ doSessCommand methodDelete ("/cookie/" `append` urlEncode (cookName c)) Null +deleteCookie c = noReturn $ doSessCommand methodDelete ("/cookie/" `append` urlEncode (cookName c)) emptyObject deleteCookieByName :: (HasCallStack, WebDriver wd) => Text -> wd () -deleteCookieByName n = noReturn $ doSessCommand methodDelete ("/cookie/" `append` n) Null +deleteCookieByName n = noReturn $ doSessCommand methodDelete ("/cookie/" `append` n) emptyObject -- |Delete all visible cookies on the current page. deleteVisibleCookies :: (HasCallStack, WebDriver wd) => wd () -deleteVisibleCookies = noReturn $ doSessCommand methodDelete "/cookie" Null +deleteVisibleCookies = noReturn $ doSessCommand methodDelete "/cookie" emptyObject -- |Get the current page source getSource :: (HasCallStack, WebDriver wd) => wd Text @@ -377,11 +416,7 @@ getTitle :: (HasCallStack, WebDriver wd) => wd Text getTitle = doSessCommand methodGet "/title" Null -- |Specifies element(s) within a DOM tree using various selection methods. -data Selector = ById Text - | ByName Text - | ByClass Text -- ^ (Note: multiple classes are not - -- allowed. For more control, use 'ByCSS') - | ByTag Text +data Selector = ByTag Text | ByLinkText Text | ByPartialLinkText Text | ByCSS Text @@ -390,9 +425,6 @@ data Selector = ById Text instance ToJSON Selector where toJSON s = case s of - ById t -> selector "id" t - ByName t -> selector "name" t - ByClass t -> selector "class name" t ByTag t -> selector "tag name" t ByLinkText t -> selector "link text" t ByPartialLinkText t -> selector "partial link text" t @@ -412,17 +444,17 @@ findElems = doSessCommand methodPost "/elements" -- |Return the element that currently has focus. activeElem :: (HasCallStack, WebDriver wd) => wd Element -activeElem = doSessCommand methodPost "/element/active" Null +activeElem = doSessCommand methodGet "/element/active" Null -- |Search for an element using the given element as root. findElemFrom :: (HasCallStack, WebDriver wd) => Element -> Selector -> wd Element -findElemFrom e s +findElemFrom e s | isRelative s = doElemCommand methodPost e "/element" s | otherwise = fail "Selector in findElemFrom must be relative" -- |Find all elements matching a selector, using the given element as root. findElemsFrom :: (HasCallStack, WebDriver wd) => Element -> Selector -> wd [Element] -findElemsFrom e s +findElemsFrom e s | isRelative s = doElemCommand methodPost e "/elements" s | otherwise = fail "Selector in findElemsFrom must be relative" @@ -430,82 +462,114 @@ isRelative :: Selector -> Bool isRelative (ByXPath t) = not $ "/" `T.isPrefixOf` t isRelative _ = True --- |Describe the element. Returns a JSON object whose meaning is currently --- undefined by the WebDriver protocol. -elemInfo :: (HasCallStack, WebDriver wd) => Element -> wd Value -elemInfo e = doElemCommand methodGet e "" Null -{-# DEPRECATED elemInfo "This command does not work with Marionette (Firefox) driver, and is likely to be completely removed in Selenium 4" #-} +performActions :: (HasCallStack, WebDriver wd) => [Actions] -> wd () +performActions = noReturn . doSessCommand methodPost "/actions" . single "actions" + +releaseActions :: (HasCallStack, WebDriver wd) => wd () +releaseActions = noReturn $ doSessCommand methodDelete "/actions" emptyObject -- |Click on an element. click :: (HasCallStack, WebDriver wd) => Element -> wd () -click e = noReturn $ doElemCommand methodPost e "/click" Null +click e = noReturn $ doElemCommand methodPost e "/click" emptyObject -- |Submit a form element. This may be applied to descendents of a form element -- as well. submit :: (HasCallStack, WebDriver wd) => Element -> wd () -submit e = noReturn $ doElemCommand methodPost e "/submit" Null +submit e = noReturn $ doElemCommand methodPost e "/submit" emptyObject -- |Get all visible text within this element. getText :: (HasCallStack, WebDriver wd) => Element -> wd Text -getText e = doElemCommand methodGet e "/text" Null +getText e = doElemCommand methodGet e "/text" emptyObject -- |Send a sequence of keystrokes to an element. All modifier keys are released -- at the end of the function. Named constants for special modifier keys can be found -- in "Test.WebDriver.Common.Keys" sendKeys :: (HasCallStack, WebDriver wd) => Text -> Element -> wd () -sendKeys t e = noReturn . doElemCommand methodPost e "/value" . single "value" $ [t] +sendKeys t e = noReturn . doElemCommand methodPost e "/value" . single "text" $ t -- |Similar to sendKeys, but doesn't implicitly release modifier keys -- afterwards. This allows you to combine modifiers with mouse clicks. sendRawKeys :: (HasCallStack, WebDriver wd) => Text -> wd () -sendRawKeys t = noReturn . doSessCommand methodPost "/keys" . single "value" $ [t] +sendRawKeys t + | t == Keys.null = + releaseActions + | otherwise = + performActions + [ Actions + { actionsType = ActionsKey + , actionsId = "keyboard" + , actionsParameters = Nothing + , actionsActions = + flip concatMap (T.chunksOf 1 t) $ \c -> + if c `elem` [Keys.control, Keys.alt, Keys.shift, Keys.meta] + then [ keyDownAction c ] + else [ keyDownAction c, keyUpAction c ] + } + ] + -- |Return the tag name of the given element. tagName :: (HasCallStack, WebDriver wd) => Element -> wd Text -tagName e = doElemCommand methodGet e "/name" Null +tagName e = doElemCommand methodGet e "/name" emptyObject -- |Clear a textarea or text input element's value. clearInput :: (HasCallStack, WebDriver wd) => Element -> wd () -clearInput e = noReturn $ doElemCommand methodPost e "/clear" Null +clearInput e = noReturn $ doElemCommand methodPost e "/clear" emptyObject -- |Determine if the element is selected. isSelected :: (HasCallStack, WebDriver wd) => Element -> wd Bool -isSelected e = doElemCommand methodGet e "/selected" Null +isSelected e = doElemCommand methodGet e "/selected" emptyObject -- |Determine if the element is enabled. isEnabled :: (HasCallStack, WebDriver wd) => Element -> wd Bool -isEnabled e = doElemCommand methodGet e "/enabled" Null +isEnabled e = doElemCommand methodGet e "/enabled" emptyObject -- |Determine if the element is displayed. isDisplayed :: (HasCallStack, WebDriver wd) => Element -> wd Bool -isDisplayed e = doElemCommand methodGet e "/displayed" Null +isDisplayed e = doElemCommand methodGet e "/displayed" emptyObject -- |Retrieve the value of an element's attribute attr :: (HasCallStack, WebDriver wd) => Element -> Text -> wd (Maybe Text) -attr e t = doElemCommand methodGet e ("/attribute/" `append` urlEncode t) Null +attr e t = doElemCommand methodGet e ("/attribute/" `append` urlEncode t) emptyObject + +-- |Retrieve the value of an element's propery +prop :: (HasCallStack, WebDriver wd) => Element -> Text -> wd (Maybe Text) +prop e t = doElemCommand methodGet e ("/property/" `append` urlEncode t) emptyObject -- |Retrieve the value of an element's computed CSS property cssProp :: (HasCallStack, WebDriver wd) => Element -> Text -> wd (Maybe Text) -cssProp e t = doElemCommand methodGet e ("/css/" `append` urlEncode t) Null +cssProp e t = doElemCommand methodGet e ("/css/" `append` urlEncode t) emptyObject -- |Retrieve an element's current position. elemPos :: (HasCallStack, WebDriver wd) => Element -> wd (Float, Float) -elemPos e = doElemCommand methodGet e "/location" Null >>= parsePair "x" "y" "elemPos" +elemPos e = do + ElemRect{..} <- elemRect e + pure (rectX, rectY) -- |Retrieve an element's current size. elemSize :: (HasCallStack, WebDriver wd) => Element -> wd (Float, Float) -elemSize e = doElemCommand methodGet e "/size" Null - >>= parsePair "width" "height" "elemSize" - -infix 4 <==> --- |Determines if two element identifiers refer to the same element. -(<==>) :: (HasCallStack, WebDriver wd) => Element -> Element -> wd Bool -e1 <==> (Element e2) = doElemCommand methodGet e1 ("/equals/" `append` urlEncode e2) Null - --- |Determines if two element identifiers refer to different elements. -infix 4 -() :: (HasCallStack, WebDriver wd) => Element -> Element -> wd Bool -e1 e2 = not <$> (e1 <==> e2) +elemSize e = do + ElemRect{..} <- elemRect e + pure (rectWidth, rectHeight) + +data ElemRect = ElemRect + { rectX :: Float + , rectY :: Float + , rectWidth :: Float + , rectHeight :: Float + } deriving (Eq, Show) + +instance FromJSON ElemRect where + parseJSON = withObject "ElemRect" $ \o -> do + rectX <- o .: "x" + rectY <- o .: "y" + rectWidth <- o .: "width" + rectHeight <- o .: "height" + pure $ ElemRect {..} + +-- |Retrieve an element's current rect. +elemRect :: (HasCallStack, WebDriver wd) => Element -> wd ElemRect +elemRect e = doElemCommand methodGet e "/rect" emptyObject -- |A screen orientation data Orientation = Landscape | Portrait @@ -531,54 +595,66 @@ setOrientation = noReturn . doSessCommand methodPost "/orientation" . single "or -- |Get the text of an alert dialog. getAlertText :: (HasCallStack, WebDriver wd) => wd Text -getAlertText = doSessCommand methodGet "/alert_text" Null +getAlertText = doSessCommand methodGet "/alert/text" Null -- |Sends keystrokes to Javascript prompt() dialog. replyToAlert :: (HasCallStack, WebDriver wd) => Text -> wd () -replyToAlert = noReturn . doSessCommand methodPost "/alert_text" . single "text" +replyToAlert = noReturn . doSessCommand methodPost "/alert/text" . single "text" -- |Accepts the currently displayed alert dialog. acceptAlert :: (HasCallStack, WebDriver wd) => wd () -acceptAlert = noReturn $ doSessCommand methodPost "/accept_alert" Null +acceptAlert = noReturn $ doSessCommand methodPost "/alert/accept" emptyObject -- |Dismisses the currently displayed alert dialog. dismissAlert :: (HasCallStack, WebDriver wd) => wd () -dismissAlert = noReturn $ doSessCommand methodPost "/dismiss_alert" Null +dismissAlert = noReturn $ doSessCommand methodPost "/alert/dismiss" emptyObject --- |Moves the mouse to the given position relative to the active element. +-- |Moves the mouse to the given position relative to current mouse position. moveTo :: (HasCallStack, WebDriver wd) => (Int, Int) -> wd () -moveTo = noReturn . doSessCommand methodPost "/moveto" . pair ("xoffset","yoffset") +moveTo (xoffset, yoffset) = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerMoveAction (xoffset, yoffset) OriginPointer + ] + } + ] -- |Moves the mouse to the center of a given element. moveToCenter :: (HasCallStack, WebDriver wd) => Element -> wd () -moveToCenter (Element e) = - noReturn . doSessCommand methodPost "/moveto" . single "element" $ e +moveToCenter = moveToFrom (0, 0) --- |Moves the mouse to the given position relative to the given element. +-- |Moves the mouse to the given position relative to the given element's top left. moveToFrom :: (HasCallStack, WebDriver wd) => (Int, Int) -> Element -> wd () -moveToFrom (x,y) (Element e) = - noReturn . doSessCommand methodPost "/moveto" - . triple ("element","xoffset","yoffset") $ (e,x,y) - --- |A mouse button -data MouseButton = LeftButton | MiddleButton | RightButton - deriving (Eq, Show, Ord, Bounded, Enum) - -instance ToJSON MouseButton where - toJSON = toJSON . fromEnum - -instance FromJSON MouseButton where - parseJSON v = do - n <- parseJSON v - case n :: Integer of - 0 -> return LeftButton - 1 -> return MiddleButton - 2 -> return RightButton - err -> fail $ "Invalid JSON for MouseButton: " ++ show err +moveToFrom (x, y) e = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerMoveAction (x, y) (OriginElement e) + ] + } + ] -- |Click at the current mouse position with the given mouse button. clickWith :: (HasCallStack, WebDriver wd) => MouseButton -> wd () -clickWith = noReturn . doSessCommand methodPost "/click" . single "button" +clickWith btn = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerDownAction { actionButton = Just btn } + , pointerUpAction { actionButton = Just btn } + ] + } + ] -- |Perform the given action with the left mouse button held down. The mouse -- is automatically released afterwards. @@ -588,15 +664,48 @@ withMouseDown wd = mouseDown >> wd <* mouseUp -- |Press and hold the left mouse button down. Note that undefined behavior -- occurs if the next mouse command is not mouseUp. mouseDown :: (HasCallStack, WebDriver wd) => wd () -mouseDown = noReturn $ doSessCommand methodPost "/buttondown" Null +mouseDown = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerDownAction { actionButton = Just LeftButton } + ] + } + ] -- |Release the left mouse button. mouseUp :: (HasCallStack, WebDriver wd) => wd () -mouseUp = noReturn $ doSessCommand methodPost "/buttonup" Null +mouseUp = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerUpAction { actionButton = Just LeftButton } + ] + } + ] -- |Double click at the current mouse location. doubleClick :: (HasCallStack, WebDriver wd) => wd () -doubleClick = noReturn $ doSessCommand methodPost "/doubleclick" Null +doubleClick = do + performActions + [ Actions + { actionsType = ActionsPointer + , actionsId = "pointer" + , actionsParameters = Just (ActionsParameters { paramsPointerType = Just PointerMouse }) + , actionsActions = + [ pointerDownAction { actionButton = Just LeftButton } + , pointerUpAction { actionButton = Just LeftButton } + , pointerDownAction { actionButton = Just LeftButton } + , pointerUpAction { actionButton = Just LeftButton } + ] + } + ] -- |Single tap on the touch screen at the given element's location. touchClick :: (HasCallStack, WebDriver wd) => Element -> wd () @@ -705,15 +814,15 @@ uploadZipEntry = doSessCommand methodPost "/file" . single "file" -- |Get the current number of keys in a web storage area. storageSize :: (HasCallStack, WebDriver wd) => WebStorageType -> wd Integer -storageSize s = doStorageCommand methodGet s "/size" Null +storageSize s = doStorageCommand methodGet s "/size" emptyObject -- |Get a list of all keys from a web storage area. getAllKeys :: (HasCallStack, WebDriver wd) => WebStorageType -> wd [Text] -getAllKeys s = doStorageCommand methodGet s "" Null +getAllKeys s = doStorageCommand methodGet s "" emptyObject -- |Delete all keys within a given web storage area. deleteAllKeys :: (HasCallStack, WebDriver wd) => WebStorageType -> wd () -deleteAllKeys s = noReturn $ doStorageCommand methodDelete s "" Null +deleteAllKeys s = noReturn $ doStorageCommand methodDelete s "" emptyObject -- |An HTML 5 storage type data WebStorageType = LocalStorage | SessionStorage @@ -723,7 +832,7 @@ data WebStorageType = LocalStorage | SessionStorage -- Unset keys result in empty strings, since the Web Storage spec -- makes no distinction between the empty string and an undefined value. getKey :: (HasCallStack, WebDriver wd) => WebStorageType -> Text -> wd Text -getKey s k = doStorageCommand methodGet s ("/key/" `T.append` urlEncode k) Null +getKey s k = doStorageCommand methodGet s ("/key/" `T.append` urlEncode k) emptyObject -- |Set a key in the given web storage area. setKey :: (HasCallStack, WebDriver wd) => WebStorageType -> Text -> Text -> wd Text @@ -731,7 +840,7 @@ setKey s k v = doStorageCommand methodPost s "" . object $ ["key" .= k, "value" .= v ] -- |Delete a key in the given web storage area. deleteKey :: (HasCallStack, WebDriver wd) => WebStorageType -> Text -> wd () -deleteKey s k = noReturn $ doStorageCommand methodPost s ("/key/" `T.append` urlEncode k) Null +deleteKey s k = noReturn $ doStorageCommand methodPost s ("/key/" `T.append` urlEncode k) emptyObject -- |A wrapper around 'doSessCommand' to create web storage requests. doStorageCommand :: (HasCallStack, WebDriver wd, ToJSON a, FromJSON b) => diff --git a/src/Test/WebDriver/Commands/Internal.hs b/src/Test/WebDriver/Commands/Internal.hs index a9dc01f..1db0bc8 100644 --- a/src/Test/WebDriver/Commands/Internal.hs +++ b/src/Test/WebDriver/Commands/Internal.hs @@ -13,7 +13,7 @@ module Test.WebDriver.Commands.Internal -- ** Commands with element :id URL parameters , doElemCommand, Element(..) -- ** Commands with :windowHandle URL parameters - , doWinCommand, WindowHandle(..), currentWindow + , WindowHandle(..) -- * Exceptions , NoSessionId(..) ) where @@ -21,7 +21,6 @@ module Test.WebDriver.Commands.Internal import Test.WebDriver.Class import Test.WebDriver.JSON import Test.WebDriver.Session -import Test.WebDriver.JSON import Test.WebDriver.Utils (urlEncode) import Control.Applicative @@ -29,7 +28,6 @@ import Control.Exception.Lifted import Data.Aeson import Data.Aeson.Types import Data.CallStack -import Data.Default.Class import Data.Text (Text) import qualified Data.Text as T import Data.Typeable @@ -41,24 +39,17 @@ newtype Element = Element Text deriving (Eq, Ord, Show, Read) instance FromJSON Element where - parseJSON (Object o) = Element <$> (o .: "ELEMENT" <|> o .: "element-6066-11e4-a52e-4f735466cecf") + parseJSON (Object o) = Element <$> o .: "element-6066-11e4-a52e-4f735466cecf" parseJSON v = typeMismatch "Element" v instance ToJSON Element where - toJSON (Element e) = object ["ELEMENT" .= e] + 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) -instance Default WindowHandle where - def = currentWindow - --- |A special 'WindowHandle' that always refers to the currently focused window. --- This is also used by the 'Default' instance. -currentWindow :: WindowHandle -currentWindow = WindowHandle "current" instance Exception NoSessionId -- |A command requiring a session ID was attempted when no session ID was @@ -98,12 +89,3 @@ 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 - --- |A wrapper around 'doSessCommand' to create window handle URLS. --- For example, passing a URL of \"/size\" will expand to --- \"/session/:sessionId/window/:windowHandle/\", where :sessionId and --- :windowHandle are URL parameters as described in the wire protocol -doWinCommand :: (HasCallStack, WebDriver wd, ToJSON a, FromJSON b) => - Method -> WindowHandle -> Text -> a -> wd b -doWinCommand m (WindowHandle w) path a = - doSessCommand m (T.concat ["/window/", urlEncode w, path]) a diff --git a/src/Test/WebDriver/Commands/Wait.hs b/src/Test/WebDriver/Commands/Wait.hs index 6f3af06..86586c7 100644 --- a/src/Test/WebDriver/Commands/Wait.hs +++ b/src/Test/WebDriver/Commands/Wait.hs @@ -70,7 +70,7 @@ expectNotStale e = catchFailedCommand StaleElementReference $ do -- | 'expect' an alert to be present on the page, and returns its text. expectAlertOpen :: (WebDriver wd, HasCallStack) => wd Text -expectAlertOpen = catchFailedCommand NoAlertOpen getAlertText +expectAlertOpen = catchFailedCommand NoSuchAlert getAlertText -- |Catches any `FailedCommand` exceptions with the given `FailedCommandType` and rethrows as 'ExpectFailed' catchFailedCommand :: (MonadBaseControl IO m, HasCallStack) => FailedCommandType -> m a -> m a diff --git a/src/Test/WebDriver/Exceptions/Internal.hs b/src/Test/WebDriver/Exceptions/Internal.hs index 7960612..0d29950 100644 --- a/src/Test/WebDriver/Exceptions/Internal.hs +++ b/src/Test/WebDriver/Exceptions/Internal.hs @@ -7,6 +7,7 @@ module Test.WebDriver.Exceptions.Internal , FailedCommand(..), failedCommand, mkFailedCommandInfo , FailedCommandType(..), FailedCommandInfo(..), StackFrame(..) + , fromTypeString, toTypeString , externalCallStack, callStackItemToStackFrame ) where import Test.WebDriver.Session @@ -61,32 +62,94 @@ data FailedCommand = FailedCommand FailedCommandType FailedCommandInfo deriving (Show, Typeable) -- |The type of failed command exception that occured. -data FailedCommandType = NoSuchElement - | NoSuchFrame - | UnknownFrame - | StaleElementReference - | ElementNotVisible +data FailedCommandType = ElementClickIntercepted + | ElementNotInteractable + | InsecureCertificate + | InvalidArgument + | InvalidCookieDomain | InvalidElementState - | UnknownError - | ElementIsNotSelectable + | InvalidSelector + | InvalidSessionId | JavascriptError - | XPathLookupError - | Timeout + | MoveTargetOutOfBounds + | NoSuchAlert + | NoSuchCookie + | NoSuchElement + | NoSuchFrame | NoSuchWindow - | InvalidCookieDomain - | UnableToSetCookie - | UnexpectedAlertOpen - | NoAlertOpen | ScriptTimeout - | InvalidElementCoordinates - | IMENotAvailable - | IMEEngineActivationFailed - | InvalidSelector | SessionNotCreated - | MoveTargetOutOfBounds - | InvalidXPathSelector - | InvalidXPathSelectorReturnType - deriving (Eq, Ord, Enum, Bounded, Show) + | StaleElementReference + | Timeout + | UnableToSetCookie + | UnableToCaptureScreen + | UnexpectedAlertOpen + | UnknownCommandType + | UnknownError + | UnknownMethod + | UnsupportedOperation + deriving (Eq, Show) + +toTypeString :: FailedCommandType -> String +toTypeString t = + case t of + ElementClickIntercepted -> "element click intercepted" + ElementNotInteractable -> "element not interactable" + InsecureCertificate -> "insecure certificate" + InvalidArgument -> "invalid argument" + InvalidCookieDomain -> "invalid cookie domain" + InvalidElementState -> "invalid element state" + InvalidSelector -> "invalid selector" + InvalidSessionId -> "invalid session id" + JavascriptError -> "javascript error" + MoveTargetOutOfBounds -> "move target out of bounds" + NoSuchAlert -> "no such alert" + NoSuchCookie -> "no such cookie" + NoSuchElement -> "no such element" + NoSuchFrame -> "no such frame" + NoSuchWindow -> "no such window" + ScriptTimeout -> "script timeout" + SessionNotCreated -> "session not created" + StaleElementReference -> "stale element reference" + Timeout -> "timeout" + UnableToSetCookie -> "unable to set cookie" + UnableToCaptureScreen -> "unable to capture screen" + UnexpectedAlertOpen -> "unexpected alert open" + UnknownCommandType -> "unknown command type" + UnknownError -> "unknown error" + UnknownMethod -> "unknown method" + UnsupportedOperation -> "unsupported operation" + +fromTypeString :: String -> FailedCommandType +fromTypeString s = + case s of + "element click intercepted" -> ElementClickIntercepted + "element not interactable" -> ElementNotInteractable + "insecure certificate" -> InsecureCertificate + "invalid argument" -> InvalidArgument + "invalid cookie domain" -> InvalidCookieDomain + "invalid element state" -> InvalidElementState + "invalid selector" -> InvalidSelector + "invalid session id" -> InvalidSessionId + "javascript error" -> JavascriptError + "move target out of bounds" -> MoveTargetOutOfBounds + "no such alert" -> NoSuchAlert + "no such cookie" -> NoSuchCookie + "no such element" -> NoSuchElement + "no such frame" -> NoSuchFrame + "no such window" -> NoSuchWindow + "script timeout" -> ScriptTimeout + "session not created" -> SessionNotCreated + "stale element reference" -> StaleElementReference + "timeout" -> Timeout + "unable to set cookie" -> UnableToSetCookie + "unable to capture screen" -> UnableToCaptureScreen + "unexpected alert open" -> UnexpectedAlertOpen + "unknown command type" -> UnknownCommandType + "unknown error" -> UnknownError + "unknown method" -> UnknownMethod + "unsupported operation" -> UnsupportedOperation + _ -> UnknownError -- |Detailed information about the failed command provided by the server. data FailedCommandInfo = @@ -94,6 +157,10 @@ data FailedCommandInfo = errMsg :: String -- |The session associated with -- the exception. + , -- |The error message. + errType :: String + -- |The session associated with + -- the exception. , errSess :: Maybe WDSession -- |A screen shot of the focused window -- when the exception occured, @@ -129,19 +196,21 @@ instance Show FailedCommandInfo where -- |Constructs a FailedCommandInfo from only an error message. -mkFailedCommandInfo :: (WDSessionState s) => String -> CallStack -> s FailedCommandInfo -mkFailedCommandInfo m cs = do +mkFailedCommandInfo :: (WDSessionState s) => FailedCommandType -> String -> CallStack -> s FailedCommandInfo +mkFailedCommandInfo t m cs = do sess <- getSession return $ FailedCommandInfo { errMsg = m + , errType = toTypeString t , errSess = Just sess , errScreen = Nothing , errClass = Nothing - , errStack = fmap callStackItemToStackFrame cs } + , errStack = fmap callStackItemToStackFrame cs + } -- |Use GHC's CallStack capabilities to return a callstack to help debug a FailedCommand. -- Drops all stack frames inside Test.WebDriver modules, so the first frame on the stack -- should be where the user called into Test.WebDriver -externalCallStack :: (HasCallStack) => CallStack +externalCallStack :: HasCallStack => CallStack externalCallStack = dropWhile isWebDriverFrame callStack where isWebDriverFrame :: ([Char], SrcLoc) -> Bool isWebDriverFrame (_, SrcLoc {srcLocModule}) = "Test.WebDriver" `L.isPrefixOf` srcLocModule @@ -150,7 +219,7 @@ externalCallStack = dropWhile isWebDriverFrame callStack -- info present. failedCommand :: (HasCallStack, WDSessionStateIO s) => FailedCommandType -> String -> s a failedCommand t m = do - throwIO . FailedCommand t =<< mkFailedCommandInfo m callStack + throwIO . FailedCommand t =<< mkFailedCommandInfo t m callStack -- |An individual stack frame from the stack trace provided by the server -- during a FailedCommand. @@ -173,6 +242,7 @@ instance Show StackFrame where instance FromJSON FailedCommandInfo where parseJSON (Object o) = FailedCommandInfo <$> (req "message" >>= maybe (return "") return) + <*> (req "error" >>= maybe (return "") return) <*> pure Nothing <*> (fmap TLE.encodeUtf8 <$> opt "screen" Nothing) <*> opt "class" Nothing diff --git a/src/Test/WebDriver/Internal.hs b/src/Test/WebDriver/Internal.hs index b048747..48663dd 100644 --- a/src/Test/WebDriver/Internal.hs +++ b/src/Test/WebDriver/Internal.hs @@ -27,10 +27,9 @@ import Network.HTTP.Types.Status (Status(..)) import qualified Data.ByteString.Base64.Lazy as B64 import qualified Data.ByteString.Char8 as BS import Data.ByteString.Lazy.Char8 (ByteString) -import Data.ByteString.Lazy.Char8 as LBS (unpack, null) -import qualified Data.ByteString.Lazy.Internal as LBS (ByteString(..)) +import Data.ByteString.Lazy.Char8 as LBS (null) import Data.CallStack -import Data.Text as T (Text, splitOn, null) +import Data.Text as T (Text) import qualified Data.Text.Encoding as TE import Control.Applicative @@ -38,21 +37,12 @@ import Control.Exception (Exception, SomeException(..), toException, fromExcepti import Control.Exception.Lifted (throwIO) import Control.Monad.Base -import Data.String (fromString) -import Data.Word (Word8) - #if !MIN_VERSION_http_client(0,4,30) import Data.Default.Class #endif import Prelude -- hides some "unused import" warnings ---This is the defintion of fromStrict used by bytestring >= 0.10; we redefine it here to support bytestring < 0.10 -fromStrict :: BS.ByteString -> LBS.ByteString -fromStrict bs | BS.null bs = LBS.Empty - | otherwise = LBS.Chunk bs LBS.Empty - - --Compatability function to support http-client < 0.4.30 defaultRequest :: Request #if MIN_VERSION_http_client(0,4,30) @@ -113,57 +103,23 @@ retryOnTimeout maxRetry go = retry' 0 -- |Parses a 'WDResponse' object from a given HTTP response. getJSONResult :: (HasCallStack, WDSessionStateControl s, FromJSON a) => Response ByteString -> s (Either SomeException a) -getJSONResult r - --malformed request errors - | code >= 400 && code < 500 = do - lastReq <- mostRecentHTTPRequest <$> getSession - returnErr . UnknownCommand . maybe reason show $ lastReq - --server-side errors - | code >= 500 && code < 600 = - case lookup hContentType headers of - Just ct - | "application/json" `BS.isInfixOf` ct -> - parseJSON' - (maybe body fromStrict $ lookup "X-Response-Body-Start" headers) - >>= handleJSONErr - >>= maybe returnNull returnErr - | otherwise -> - returnHTTPErr ServerError - Nothing -> - returnHTTPErr (ServerError . ("HTTP response missing content type. Server reason was: "++)) - --redirect case (used as a response to createSession requests) - | code == 302 || code == 303 = - case lookup hLocation headers of - Nothing -> returnErr . HTTPStatusUnknown code $ LBS.unpack body - Just loc -> do - let sessId = last . filter (not . T.null) . splitOn "/" . fromString $ BS.unpack loc - modifySession $ \sess -> sess {wdSessId = Just (SessionId sessId)} - returnNull - -- No Content response - | code == 204 = returnNull - -- HTTP Success - | code >= 200 && code < 300 = - if LBS.null body - then returnNull - else do - rsp@WDResponse {rspVal = val} <- parseJSON' body - handleJSONErr rsp >>= maybe - (handleRespSessionId rsp >> Right <$> fromJSON' val) - returnErr - -- other status codes: return error - | otherwise = returnHTTPErr (HTTPStatusUnknown code) +getJSONResult r = + if LBS.null body + then returnNull + else do + rsp@WDResponse {rspVal = val} <- parseJSON' body + if code == 200 + then handleRespSessionId rsp >> Right <$> fromJSON' val + else handleJSONErr rsp >>= returnErr where --helper functions returnErr :: (Exception e, Monad m) => e -> m (Either SomeException a) returnErr = return . Left . toException - returnHTTPErr errType = returnErr . errType $ reason returnNull = Right <$> fromJSON' Null --HTTP response variables code = statusCode status - reason = BS.unpack $ statusMessage status status = responseStatus r body = responseBody r - headers = responseHeaders r handleRespSessionId :: (HasCallStack, WDSessionStateIO s) => WDResponse -> s () handleRespSessionId WDResponse{rspSessId = sessId'} = do @@ -176,9 +132,8 @@ handleRespSessionId WDResponse{rspSessId = sessId'} = do ++ ") does not match local session ID (" ++ show sessId ++ ")" _ -> return () -handleJSONErr :: (HasCallStack, WDSessionStateControl s) => WDResponse -> s (Maybe SomeException) -handleJSONErr WDResponse{rspStatus = 0} = return Nothing -handleJSONErr WDResponse{rspVal = val, rspStatus = status} = do +handleJSONErr :: (HasCallStack, WDSessionStateControl s) => WDResponse -> s SomeException +handleJSONErr WDResponse{rspVal = val} = do sess <- getSession errInfo <- fromJSON' val let screen = B64.decodeLenient <$> errScreen errInfo @@ -186,47 +141,18 @@ handleJSONErr WDResponse{rspVal = val, rspStatus = status} = do errInfo' = errInfo { errSess = Just sess -- Append the Haskell stack frames to the ones returned from Selenium , errScreen = screen - , errStack = seleniumStack ++ (fmap callStackItemToStackFrame callStack) } + , errStack = seleniumStack ++ fmap callStackItemToStackFrame callStack } e errType = toException $ FailedCommand errType errInfo' - return . Just $ case status of - 7 -> e NoSuchElement - 8 -> e NoSuchFrame - 9 -> toException . UnknownCommand . errMsg $ errInfo - 10 -> e StaleElementReference - 11 -> e ElementNotVisible - 12 -> e InvalidElementState - 13 -> e UnknownError - 15 -> e ElementIsNotSelectable - 17 -> e JavascriptError - 19 -> e XPathLookupError - 21 -> e Timeout - 23 -> e NoSuchWindow - 24 -> e InvalidCookieDomain - 25 -> e UnableToSetCookie - 26 -> e UnexpectedAlertOpen - 27 -> e NoAlertOpen - 28 -> e ScriptTimeout - 29 -> e InvalidElementCoordinates - 30 -> e IMENotAvailable - 31 -> e IMEEngineActivationFailed - 32 -> e InvalidSelector - 33 -> e SessionNotCreated - 34 -> e MoveTargetOutOfBounds - 51 -> e InvalidXPathSelector - 52 -> e InvalidXPathSelectorReturnType - _ -> e UnknownError - + return $ e $ fromTypeString $ errType errInfo -- |Internal type representing the JSON response object data WDResponse = WDResponse { rspSessId :: Maybe SessionId - , rspStatus :: Word8 , rspVal :: Value } deriving (Eq, Show) instance FromJSON WDResponse where parseJSON (Object o) = WDResponse <$> o .:?? "sessionId" .!= Nothing - <*> o .: "status" <*> o .:?? "value" .!= Null parseJSON v = typeMismatch "WDResponse" v diff --git a/src/Test/WebDriver/JSON/Internal.hs b/src/Test/WebDriver/JSON/Internal.hs new file mode 100644 index 0000000..5fd3e80 --- /dev/null +++ b/src/Test/WebDriver/JSON/Internal.hs @@ -0,0 +1,7 @@ +module Test.WebDriver.JSON.Internal where + +import qualified Data.Char as C + +lower1 :: String -> String +lower1 [] = [] +lower1 (c:cs) = C.toLower c : cs diff --git a/src/Test/WebDriver/Monad.hs b/src/Test/WebDriver/Monad.hs index f79615d..25c56a0 100644 --- a/src/Test/WebDriver/Monad.hs +++ b/src/Test/WebDriver/Monad.hs @@ -21,11 +21,8 @@ import Test.WebDriver.Config import Test.WebDriver.Internal import Test.WebDriver.Session -#if MIN_VERSION_base(4,9,0) -import qualified Control.Monad.Fail as Fail -#endif - import Control.Monad.Base (MonadBase, liftBase) +import Control.Monad.Fail import Control.Monad.Fix import Control.Monad.IO.Class import Control.Monad.Trans.Control (MonadBaseControl(..), StM) @@ -36,7 +33,7 @@ import Control.Applicative import Data.CallStack -import Prelude -- hides some "unused import" warnings +import Prelude hiding (fail) -- hides some "unused import" warnings {- | A state monad for WebDriver commands. -} @@ -46,10 +43,8 @@ newtype WD a = WD (StateT WDSession IO a) instance MonadBase IO WD where liftBase = WD . liftBase -#if MIN_VERSION_base(4,9,0) -instance Fail.MonadFail WD where - fail s = WD $ Fail.fail s -#endif +instance MonadFail WD where + fail s = WD $ fail s instance MonadBaseControl IO WD where #if MIN_VERSION_monad_control(1,0,0) diff --git a/src/Test/WebDriver/Types.hs b/src/Test/WebDriver/Types.hs index 3e2233d..549e6f3 100644 --- a/src/Test/WebDriver/Types.hs +++ b/src/Test/WebDriver/Types.hs @@ -20,7 +20,7 @@ module Test.WebDriver.Types ( -- * WebDriver objects and command-specific types , Element(..) - , WindowHandle(..), currentWindow + , WindowHandle(..) , Selector(..) , JSArg(..) , FrameSelector(..) diff --git a/test/etc/SearchBaidu.hs b/test/etc/SearchBaidu.hs index fdf27be..a1d5d5b 100644 --- a/test/etc/SearchBaidu.hs +++ b/test/etc/SearchBaidu.hs @@ -33,7 +33,7 @@ searchBaidu = do container <- findElem (ById "container") eList1 <- findElems (ByCSS "c-container") eList2 <- findElems (ByClass "c-container") - expect =<< (fmap and $ zipWithM (<==>) eList1 eList2) + expect =<< (fmap and $ zipWithM (==) eList1 eList2) forM_ eList1 $ \e -> findElemsFrom e (ByTag "a") diff --git a/webdriver.cabal b/webdriver.cabal index 47e4b72..dbcc09b 100644 --- a/webdriver.cabal +++ b/webdriver.cabal @@ -43,6 +43,7 @@ source-repository head library exposed-modules: Test.WebDriver + Test.WebDriver.Actions.Internal Test.WebDriver.Capabilities Test.WebDriver.Chrome.Extension Test.WebDriver.Class @@ -58,6 +59,7 @@ library Test.WebDriver.Firefox.Profile Test.WebDriver.Internal Test.WebDriver.JSON + Test.WebDriver.JSON.Internal Test.WebDriver.Monad Test.WebDriver.Session Test.WebDriver.Session.History