happybara (empty) → 0.0.1
raw patch · 9 files changed
+1152/−0 lines, 9 filesdep +aesondep +basedep +filepathsetup-changed
Dependencies added: aeson, base, filepath, http-types, lifted-base, monad-control, mtl, text, time, transformers, transformers-base
Files
- LICENSE +19/−0
- Setup.hs +2/−0
- happybara.cabal +55/−0
- src/Happybara.hs +18/−0
- src/Happybara/Driver.hs +66/−0
- src/Happybara/Exceptions.hs +53/−0
- src/Happybara/Monad.hs +503/−0
- src/Happybara/Query.hs +293/−0
- src/Happybara/XPath.hs +143/−0
+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 2014 Charles Strahan++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ happybara.cabal view
@@ -0,0 +1,55 @@+name: happybara+version: 0.0.1+homepage: https://github.com/cstrahan/happybara+license: MIT+license-file: LICENSE+author: Charles Strahan+maintainer: charles.c.strahan@gmail.com+copyright: Copyright (c) 2014 Charles Strahan+category: Development+build-type: Simple+cabal-version: >=1.10+synopsis: Acceptance test framework for web applications+description:+ /About/+ .+ Happybara is an acceptance test framework inspired by the popular Ruby library+ \"Capybara\". A short example of Happybara's expressive DSL:+ .+ >visit "http://happybara-is-awesome.com"+ >within $ xpath "//form[@id='vote-for-happybara']" $ do+ > fillIn (fillableField "First Name" [ ]) "Bob"+ > fillIn (fillableField "Last Name" [ ]) "Smith"+ >+ > click $ button "Vote!" [ disabled False ]+ .+ /Learning Happybara/+ .+ I would suggest start with these resources (in order):+ .+ * <https://github.com/cstrahan/happybara/ The Happybara Readme>+ .+ * <http://hackage.haskell.org/package/happybara/docs/Happybara.html The Happybara Module docs>+ .+ Happy web testing!++library+ exposed-modules: Happybara,+ Happybara.Driver,+ Happybara.Exceptions,+ Happybara.Monad,+ Happybara.Query,+ Happybara.XPath+ build-depends: base >=4.6 && <4.7,+ mtl,+ monad-control,+ lifted-base,+ transformers,+ transformers-base,+ time,+ text,+ http-types,+ filepath,+ aeson+ hs-source-dirs: src+ default-language: Haskell2010
+ src/Happybara.hs view
@@ -0,0 +1,18 @@+-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+module Happybara+ ( module Happybara.Monad+ , module Happybara.Query+ , module Happybara.Driver+ , module Happybara.Exceptions+ ) where++import Happybara.Monad+import Happybara.Query+import Happybara.Driver (Driver, FrameSelector (..), Node,+ NodeValue (..))+import Happybara.Exceptions
+ src/Happybara/Driver.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE TypeFamilies #-}++-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+module Happybara.Driver where++import Data.Aeson+import Data.Text (Text)++import Network.HTTP.Types (Header, ResponseHeaders, Status)++import Happybara.Exceptions+import qualified Happybara.XPath as X++data NodeValue = SingleValue Text+ | MultiValue [Text]+ deriving (Eq, Show)++data FrameSelector = FrameIndex Int+ | FrameName Text+ | DefaultFrame+ deriving (Eq, Show)++class Driver sess where+ data Node sess :: *+ currentUrl :: sess -> IO Text+ visit :: sess -> Text -> IO ()+ findXPath :: sess -> Text -> IO [Node sess]+ findCSS :: sess -> Text -> IO [Node sess]+ html :: sess -> IO Text+ goBack :: sess -> IO ()+ goForward :: sess -> IO ()+ executeScript :: sess -> Text -> IO ()+ evaluateScript :: sess -> Text -> IO Value+ saveScreenshot :: sess -> Text -> Int -> Int -> IO ()+ responseHeaders :: sess -> IO ResponseHeaders+ statusCode :: sess -> IO Status+ setFrameFocus :: sess -> FrameSelector -> IO ()+ setWindowFocus :: sess -> Text -> IO ()+ reset :: sess -> IO ()+ findXPathRel :: sess -> Node sess -> Text -> IO [Node sess]+ findCSSRel :: sess -> Node sess -> Text -> IO [Node sess]+ allText :: sess -> Node sess -> IO Text+ visibleText :: sess -> Node sess -> IO Text+ attr :: sess -> Node sess -> Text -> IO (Maybe Text)+ getValue :: sess -> Node sess -> IO NodeValue+ setValue :: sess -> Node sess -> NodeValue -> IO ()+ selectOption :: sess -> Node sess -> IO ()+ unselectOption :: sess -> Node sess -> IO ()+ click :: sess -> Node sess -> IO ()+ rightClick :: sess -> Node sess -> IO ()+ doubleClick :: sess -> Node sess -> IO ()+ hover :: sess -> Node sess -> IO ()+ dragTo :: sess -> Node sess -> Node sess -> IO ()+ tagName :: sess -> Node sess -> IO Text+ isVisible :: sess -> Node sess -> IO Bool+ isChecked :: sess -> Node sess -> IO Bool+ isSelected :: sess -> Node sess -> IO Bool+ isDisabled :: sess -> Node sess -> IO Bool+ path :: sess -> Node sess -> IO Text+ trigger :: sess -> Node sess -> Text -> IO ()+ nodeEq :: sess -> Node sess -> Node sess -> IO Bool -- infix 4 <==>
+ src/Happybara/Exceptions.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE ExistentialQuantification #-}++-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+module Happybara.Exceptions where++import Control.Exception+import Data.Typeable++data HappybaraException = forall e . Exception e => HappybaraException e+ deriving Typeable+instance Show HappybaraException where+ show (HappybaraException e) = show e+instance Exception HappybaraException++data InvalidElementException = forall e . Exception e => InvalidElementException e+ deriving Typeable+instance Show InvalidElementException where+ show (InvalidElementException e) = show e+instance Exception InvalidElementException where+ toException = toException . HappybaraException+ fromException x = do+ HappybaraException a <- fromException x+ cast a++data ExpectationNotMetException = ExpectationNotMetException+ deriving (Show, Typeable)+instance Exception ExpectationNotMetException where+ toException = toException . InvalidElementException+ fromException x = do+ InvalidElementException a <- fromException x+ cast a++data ElementNotFoundException = ElementNotFoundException+ deriving (Show, Typeable)+instance Exception ElementNotFoundException where+ toException = toException . InvalidElementException+ fromException x = do+ InvalidElementException a <- fromException x+ cast a++data AmbiguousElementException = AmbiguousElementException+ deriving (Show, Typeable)+instance Exception AmbiguousElementException where+ toException = toException . InvalidElementException+ fromException x = do+ InvalidElementException a <- fromException x+ cast a
+ src/Happybara/Monad.hs view
@@ -0,0 +1,503 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}+++-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+module Happybara.Monad+ ( -- * Happybara Monad+ HappybaraT(..)+ , Happybara+ , runHappybaraT+ , runHappybara+ -- * Monad Settings & State+ , Exactness(..)+ , SingleMatchStrategy(..)+ , setWait+ , getWait+ , setExactness+ , getExactness+ , setSingleMatchStrategy+ , getSingleMatchStrategy+ , getDriver+ , withDriver+ , getCurrentNode+ , HappybaraState(..)+ -- * Browser State+ , visit+ , currentUrl+ , responseHeaders+ , statusCode+ , html+ , goBack+ , goForward+ , reset+ , saveScreenshot+ -- * Scoping+ , withinNode+ , withinPage+ , withinFrame+ , withinWindow+ -- * JavaScript Execution+ , executeScript+ , evaluateScript+ -- * Primitive Queries+ , findXPath+ , findCSS+ -- * Node Manipulation+ , allText+ , visibleText+ , attr+ , getValue+ , setValue+ , selectOption+ , unselectOption+ , click+ , rightClick+ , doubleClick+ , hover+ , dragTo+ , tagName+ , isVisible+ , isChecked+ , isSelected+ , isDisabled+ , path+ , trigger+ , nodeEq+ -- * Async Support+ , synchronize+ ) where++import Data.Aeson+import Data.Text (Text)+import Data.Time.Clock++import Control.Applicative+import Control.Concurrent+import Control.Exception.Lifted+import Control.Monad+import Control.Monad.Base+import Control.Monad.Identity+import Control.Monad.IO.Class+import Control.Monad.Trans.Class+import Control.Monad.Trans.Control+import Control.Monad.Trans.State++import Network.HTTP.Types (Header, ResponseHeaders, Status)++import Happybara.Driver (Driver, FrameSelector (..), Node,+ NodeValue (..))+import qualified Happybara.Driver as D+import Happybara.Exceptions++-- |+-- The Happybara monad transformer.+--+-- Requirements:+--+-- * The /sess/ session type must be an instance of 'Driver'.+--+-- * The inner monad /m/ must be an instance of 'MonadBase' 'IO' /m/, 'MonadIO' /m/, and+-- 'MonadBaseControl' 'IO' /m/.+newtype HappybaraT sess m a = HappybaraT { unHappybaraT :: StateT (HappybaraState sess) m a }+ deriving (Functor, Applicative, Monad, MonadTrans, MonadIO)++-- |+-- If you don't want to transform an existing monad, this type synonym+-- conveniently sets the inner monad to 'IO'.+type Happybara sess a = HappybaraT sess IO a++-- |+-- The exactness requirement when using the 'Happybara.Query.Query' DSL.+data Exactness = Exact -- ^ Find elements that match exactly.+ | PreferExact -- ^ First try to find exact matches;+ -- if that fails, fall back to inexact matches.+ | Inexact -- ^ Find all elements that partially match - e.g.+ -- the given string is infix of (but not necessarily equal to)+ -- whatever property (id, attribute, etc) is being queried over.+ deriving (Eq, Ord, Show)++-- |+-- This controls the 'Happybara.Query.Query' behavior of 'Happybara.Query.findOrFail' in+-- the presence of multiple matches.+data SingleMatchStrategy = MatchFirst -- ^ If no elements matched, throw 'ElementNotFoundException';+ -- otherwise, return the first matching element.+ | MatchOne -- ^ If no elements matched, throw 'ElementNotFoundException';+ -- if more than element matches, throw 'AmbiguousElementException'.++data HappybaraState sess = HappybaraState { hsDriver :: sess+ , hsWait :: Double+ , hsExactness :: Exactness+ , hsIsSynced :: Bool+ , hsSingleMatchStrategy :: SingleMatchStrategy+ , hsCurrentNode :: Maybe (Node sess)+ }++instance (MonadBase b m) => MonadBase b (HappybaraT sess m) where+ liftBase = lift . liftBase++instance MonadTransControl (HappybaraT sess) where+ newtype StT (HappybaraT sess) a = StHappybara {unStHappybara :: StT (StateT (HappybaraState sess)) a}+ liftWith = defaultLiftWith HappybaraT unHappybaraT StHappybara+ restoreT = defaultRestoreT HappybaraT unStHappybara+ {-# INLINE liftWith #-}+ {-# INLINE restoreT #-}++instance (MonadBaseControl b m) => MonadBaseControl b (HappybaraT sess m) where+ newtype StM (HappybaraT sess m) a = StMHappybara {unStMHappybara :: ComposeSt (HappybaraT sess) m a}+ liftBaseWith = defaultLiftBaseWith StMHappybara+ restoreM = defaultRestoreM unStMHappybara+ {-# INLINE liftBaseWith #-}+ {-# INLINE restoreM #-}++-- | Evaluate the happybara computation.+runHappybara :: (Driver sess) => sess -> Happybara sess a -> IO a+runHappybara = runHappybaraT++-- | Evaluate the happybara computation.+runHappybaraT :: (Driver sess, MonadIO m, MonadBase IO m, MonadBaseControl IO m)+ => sess -> HappybaraT sess m a -> m a+runHappybaraT sess act =+ evalStateT (unHappybaraT act) initialState+ where+ initialState = HappybaraState { hsDriver = sess+ , hsWait = 2+ , hsExactness = Inexact+ , hsIsSynced = False+ , hsSingleMatchStrategy = MatchOne+ , hsCurrentNode = Nothing+ }++-- | Set the number of seconds to wait between retrying an action. See+-- 'synchronize'.+setWait :: (Monad m) => Double -> HappybaraT sess m ()+setWait time =+ HappybaraT $ modify $ \s -> s { hsWait = time }++-- | Get the number of seconds to wait between retrying an action. See+-- 'synchronize'.+getWait :: (Functor m, Monad m) => HappybaraT sess m Double+getWait =+ HappybaraT $ hsWait <$> get++-- | Set the required level of exactness for queries. See 'Exactness'.+setExactness :: (Monad m) => Exactness -> HappybaraT sess m ()+setExactness exact =+ HappybaraT $ modify $ \s -> s { hsExactness = exact }++-- | Get the required level of exactness for queries. See 'Exactness'.+getExactness :: (Functor m, Monad m) => HappybaraT sess m Exactness+getExactness =+ HappybaraT $ hsExactness <$> get++-- | Use a different driver for the given action.+--+-- /Note:/ This sets the current scope via 'withinPage' before invoking the+-- action, because the current node was acquired from a different driver+-- instance. Similarly, it's a bad idea to return a 'Node' from this new driver,+-- as you might try to use it with the wrong driver instance.+withDriver :: (Driver sess, Functor m, Monad m)+ => sess -> HappybaraT sess m a -> HappybaraT sess m a+withDriver driver act = do+ oldDriver <-getDriver+ HappybaraT $ modify $ \s -> s { hsDriver = driver }+ result <- withinPage act+ HappybaraT $ modify $ \s -> s { hsDriver = oldDriver }+ return result++getDriver :: (Functor m, Monad m) => HappybaraT sess m sess+getDriver =+ HappybaraT $ hsDriver <$> get++-- | Set the query matching strategy. See 'SingleMatchStrategy'.+setSingleMatchStrategy :: (Monad m) => SingleMatchStrategy -> HappybaraT sess m ()+setSingleMatchStrategy strategy =+ HappybaraT $ modify $ \s -> s { hsSingleMatchStrategy = strategy }++-- | Get the query matching strategy. See 'SingleMatchStrategy'.+getSingleMatchStrategy :: (Functor m, Monad m) => HappybaraT sess m SingleMatchStrategy+getSingleMatchStrategy =+ HappybaraT $ hsSingleMatchStrategy <$> get++-- | Get the node that all queries are currently relative to.+getCurrentNode :: (Driver sess, Functor m, Monad m) => HappybaraT sess m (Maybe (Node sess))+getCurrentNode =+ HappybaraT $ hsCurrentNode <$> get++-- | Make all queries relative to the supplied node within the given action.+withinNode :: (Driver sess, Functor m, Monad m)+ => Node sess -> HappybaraT sess m a -> HappybaraT sess m a+withinNode newNode act = do+ oldNode <- getCurrentNode+ HappybaraT $ modify $ \s -> s { hsCurrentNode = Just newNode }+ res <- act+ HappybaraT $ modify $ \s -> s { hsCurrentNode = oldNode }+ return res++-- | Make all queries relative to the document in the given action.+withinPage :: (Driver sess, Functor m, Monad m)+ => HappybaraT sess m a -> HappybaraT sess m a+withinPage act = do+ oldNode <- getCurrentNode+ HappybaraT $ modify $ \s -> s { hsCurrentNode = Nothing }+ res <- act+ HappybaraT $ modify $ \s -> s { hsCurrentNode = oldNode }+ return res++-- | Invoke the given action until:+--+-- * The action no longer throws 'InvalidElementException', or+--+-- * The total duration of the attempts excedes the number of seconds specified by 'getWait', in+-- which case the exception is rethrown.+--+-- A couple notes:+--+-- * The action is retried every 0.05 seconds.+--+-- * To prevent exponential retrying, any inner calls to 'synchronize' are+-- ignored.+--+-- * Unless you're doing something advanced,+-- like implementing custom 'Happybara.Query.Query' instances,+-- you probably don't need to invoke this directly.+synchronize :: (Functor m, Monad m, MonadIO m, MonadBase IO m, MonadBaseControl IO m)+ => HappybaraT sess m a -> HappybaraT sess m a+synchronize act = do+ synced <- HappybaraT $ hsIsSynced <$> get+ if synced+ then act+ else do+ startTime <- liftIO getCurrentTime+ maxWait <- getWait+ HappybaraT $ modify $ \s -> s { hsIsSynced = True }+ res <- retry startTime maxWait+ HappybaraT $ modify $ \s -> s { hsIsSynced = False }+ return res+ where+ delayMicros = 50000 -- 0.05 seconds+ retry startTime maxWait =+ act `catch` (\(e :: InvalidElementException) -> do+ currenTime <- liftBase getCurrentTime+ let elapsedSeconds = fromRational $ toRational $ currenTime `diffUTCTime` startTime+ if elapsedSeconds > maxWait+ then throw e+ else do+ liftBase $ threadDelay delayMicros+ retry startTime maxWait)++-- wrapped driver methods++currentUrl :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m Text+currentUrl = do+ driver <- getDriver+ liftBase $ D.currentUrl driver++visit :: (Driver sess, Monad m, MonadBase IO m) => Text -> HappybaraT sess m ()+visit url = do+ driver <- getDriver+ liftBase $ D.visit driver url++findXPath :: (Driver sess, Monad m, MonadBase IO m) => Text -> HappybaraT sess m [Node sess]+findXPath xpath = do+ driver <- getDriver+ currentNode <- getCurrentNode+ case currentNode of+ Just node -> liftBase $ D.findXPathRel driver node xpath+ Nothing -> liftBase $ D.findXPath driver xpath++findCSS :: (Driver sess, Monad m, MonadBase IO m) => Text -> HappybaraT sess m [Node sess]+findCSS css = do+ driver <- getDriver+ currentNode <- getCurrentNode+ case currentNode of+ Just node -> liftBase $ D.findCSSRel driver node css+ Nothing -> liftBase $ D.findCSS driver css++html :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m Text+html = do+ driver <- getDriver+ liftBase $ D.html driver++goBack :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m ()+goBack = do+ driver <- getDriver+ liftBase $ D.goBack driver++goForward :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m ()+goForward = do+ driver <- getDriver+ liftBase $ D.goForward driver++executeScript :: (Driver sess, Monad m, MonadBase IO m) => Text -> HappybaraT sess m ()+executeScript script = do+ driver <- getDriver+ liftBase $ D.executeScript driver script++evaluateScript :: (Driver sess, Monad m, MonadBase IO m) => Text -> HappybaraT sess m Value+evaluateScript script = do+ driver <- getDriver+ liftBase $ D.evaluateScript driver script++saveScreenshot :: (Driver sess, Monad m, MonadBase IO m) => Text -> Int -> Int -> HappybaraT sess m ()+saveScreenshot path width height = do+ driver <- getDriver+ liftBase $ D.saveScreenshot driver path width height++responseHeaders :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m ResponseHeaders+responseHeaders = do+ driver <- getDriver+ liftBase $ D.responseHeaders driver++statusCode :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m Status+statusCode = do+ driver <- getDriver+ liftBase $ D.statusCode driver++withinFrame :: (Driver sess, Monad m, MonadBase IO m, MonadBaseControl IO m)+ => FrameSelector -> HappybaraT sess m Status -> HappybaraT sess m Status+withinFrame frameId act = do+ driver <- getDriver+ bracket_+ (liftBase $ D.setFrameFocus driver frameId)+ (liftBase $ D.setFrameFocus driver DefaultFrame)+ (act)++withinWindow :: (Driver sess, Monad m, MonadBase IO m, MonadBaseControl IO m)+ => FrameSelector -> HappybaraT sess m Status -> HappybaraT sess m Status+withinWindow windowId act = error "NOT IMPLEMENTED"++reset :: (Driver sess, Monad m, MonadBase IO m) => HappybaraT sess m ()+reset = do+ driver <- getDriver+ liftBase $ D.reset driver++allText :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Text+allText node = do+ driver <- getDriver+ liftBase $ D.allText driver node++visibleText :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Text+visibleText node = do+ driver <- getDriver+ liftBase $ D.visibleText driver node++attr :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> Text -> HappybaraT sess m (Maybe Text)+attr node name = do+ driver <- getDriver+ liftBase $ D.attr driver node name++getValue :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m NodeValue+getValue node = do+ driver <- getDriver+ liftBase $ D.getValue driver node++setValue :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> NodeValue -> HappybaraT sess m ()+setValue node val = do+ driver <- getDriver+ liftBase $ D.setValue driver node val++selectOption :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+selectOption node = do+ driver <- getDriver+ liftBase $ D.selectOption driver node++unselectOption :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+unselectOption node = do+ driver <- getDriver+ liftBase $ D.unselectOption driver node++click :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+click node = do+ driver <- getDriver+ liftBase $ D.click driver node++rightClick :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+rightClick node = do+ driver <- getDriver+ liftBase $ D.rightClick driver node++doubleClick :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+doubleClick node = do+ driver <- getDriver+ liftBase $ D.doubleClick driver node++hover :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m ()+hover node = do+ driver <- getDriver+ liftBase $ D.hover driver node++dragTo :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> Node sess -> HappybaraT sess m ()+dragTo node other = do+ driver <- getDriver+ liftBase $ D.dragTo driver node other++tagName :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Text+tagName node = do+ driver <- getDriver+ liftBase $ D.tagName driver node++isVisible :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Bool+isVisible node = do+ driver <- getDriver+ liftBase $ D.isVisible driver node++isChecked :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Bool+isChecked node = do+ driver <- getDriver+ liftBase $ D.isChecked driver node++isSelected :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Bool+isSelected node = do+ driver <- getDriver+ liftBase $ D.isSelected driver node++isDisabled :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Bool+isDisabled node = do+ driver <- getDriver+ liftBase $ D.isDisabled driver node++path :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> HappybaraT sess m Text+path node = do+ driver <- getDriver+ liftBase $ D.path driver node++trigger :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> Text -> HappybaraT sess m ()+trigger node event = do+ driver <- getDriver+ liftBase $ D.trigger driver node event++nodeEq :: (Driver sess, Monad m, MonadBase IO m)+ => Node sess -> Node sess -> HappybaraT sess m Bool+nodeEq node other = do+ driver <- getDriver+ liftBase $ D.nodeEq driver node other
+ src/Happybara/Query.hs view
@@ -0,0 +1,293 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+module Happybara.Query+ ( -- * Query Interface+ Query(find, findOrFail, findAll)+ -- * Scoping+ , within+ , withinAll+ -- * Basic Queries+ -- $queries+ , link+ , button+ , linkOrButton+ , fieldset+ , field+ , fillableField+ , select+ , checkbox+ , radioButton+ , fileField+ , optgroup+ , option+ , table+ , definitionDescription+ -- * Predicates+ , href+ , checked+ , unchecked+ , disabled+ , selected+ , options+ , elemType+ -- * Types+ , SimpleQuery(..)+ ) where++import Control.Applicative+import Control.Exception.Lifted+import Control.Monad+import Control.Monad.Base+import Control.Monad.IO.Class+import Control.Monad.Trans.Control++import Data.List (sort)+import Data.Text (Text)+import qualified Data.Text as T++import Happybara.Driver (Driver, Node, NodeValue (..))+import qualified Happybara.Driver as D+import Happybara.Exceptions+import Happybara.Monad+import qualified Happybara.Monad as M+import qualified Happybara.XPath as X++-- | This class is the backbone of Happybara's DOM querying DSL.+-- While Happybara includes support for a number of common queries, you're more than+-- welcome to implement your own 'Query' instances, thus extending the DSL.+--+-- Queries are scoped to the current node as given by 'M.getCurrentNode', and+-- a new scope can be specified via 'within'.+--+-- Note that the behavior of a query is dependent on the current 'M.Exactness' setting:+--+-- * 'M.Exact' - Find elements that match exactly.+--+-- * 'M.PreferExact' - First try to find exact matches; if that fails, fall+-- back to inexact matches.+--+-- * 'M.Inexact' - Find all elements that partially match - e.g. the given+-- string is infix of (but not necessarily equal to) whatever property (id,+-- attribute, etc) is being queried over.+--+-- When locating a single item, the failure mode depends on the current 'SingleMatchStrategy' setting:+--+-- * 'MatchFirst' - If no elements matched, throw 'ElementNotFoundException';+-- otherwise, return the first matching element.+--+-- * 'MatchOne' - If no elements matched, throw 'ElementNotFoundException'; if+-- more than element matches, throw 'AmbiguousElementException'.+--+-- To set the current 'M.Exactness', use 'M.setExactness'.+-- To set the current 'M.SingleMatchStrategy', use 'M.setSingleMatchStrategy'.+class (Driver sess, MonadIO m, MonadBase IO m, MonadBaseControl IO m) => Query q sess m where+ find :: q sess m -> HappybaraT sess m (Maybe (Node sess))+ findOrFail :: q sess m -> HappybaraT sess m (Node sess)+ findAll :: q sess m -> HappybaraT sess m [Node sess]++data SimpleQuery sess m = SimpleQuery { sqXPath :: (Bool -> Text)+ , sqPredicates :: [(Node sess) -> HappybaraT sess m Bool]+ , sqDescription :: Text+ }++instance (Driver sess, MonadIO m, MonadBase IO m, MonadBaseControl IO m)+ => Query SimpleQuery sess m where+ find q = do+ (Just <$> findOrFail q) `catch` (\(e :: InvalidElementException) ->+ return $ Nothing)++ findOrFail q = do+ M.synchronize $ do+ matchStrategy <- M.getSingleMatchStrategy+ results <- findAll q+ when (null results) $ do+ liftBase $ throw ElementNotFoundException+ case matchStrategy of+ MatchFirst -> return $ head results+ MatchOne -> do+ if isAmbiguous results+ then liftBase $ throw AmbiguousElementException+ else return $ head results+ where+ isAmbiguous (n1:n2:_) = True+ isAmbiguous _ = False++ findAll (SimpleQuery xpath preds _) = do+ exactness <- M.getExactness+ case exactness of+ Exact -> do+ find $ xpath True+ PreferExact -> do+ res <- find $ xpath True+ if null res+ then find $ xpath False+ else return res+ Inexact -> do+ find $ xpath False+ where+ allM _ [] = return True+ allM f (b:bs) = (f b) >>= (\bv -> if bv then allM f bs else return False)+ compositePredicate n = allM (\p -> p n) preds+ find x = do+ res <- M.findXPath x+ filterM compositePredicate res++-- | Set the current element scope to the element given by the query.+within :: (Query q sess m, Driver sess, Functor m, Monad m)+ => q sess m -> HappybaraT sess m a -> HappybaraT sess m a+within query act = do+ newNode <- findOrFail query+ M.withinNode newNode act++-- | For each element given by the query, set the current scope accordingly and+-- invoke the monadic action, yielding each result.+withinAll :: (Query q sess m, Driver sess, Functor m, Monad m)+ => q sess m -> HappybaraT sess m a -> HappybaraT sess m [a]+withinAll query act = do+ nodes <- findAll query+ mapM (flip M.withinNode act) nodes++-- $queries+-- Happybara includes a number of queries for common cases where you want to+-- find an element by value, title, id, alt-text, etc.+-- These queries can be further filtered by the predicates listed below.+--+-- For example, this query will return all enabled buttons matching \"Submit Application\":+--+-- @+--'findAll' $ 'button' \"Submit Application\" [ 'disabled' False ]+-- @++mkQuery :: (Driver sess) => Text -> Text -> (Text -> Bool -> Text) -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+mkQuery ty locator xpath preds =+ SimpleQuery (xpath locator) preds (locatorDescription ty locator)+ where+ escapeText = T.pack . show+ locatorDescription ty locator =+ T.concat [ty, ": ", escapeText locator]++link :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+link locator preds =+ mkQuery "link" locator X.link preds++button :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+button locator preds =+ mkQuery "button" locator X.button preds++linkOrButton :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+linkOrButton locator preds =+ mkQuery "linkOrButton" locator X.linkOrButton preds++fieldset :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+fieldset locator preds =+ mkQuery "fieldset" locator X.fieldset preds++field :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+field locator preds =+ mkQuery "field" locator X.field preds++fillableField :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+fillableField locator preds =+ mkQuery "fillableField" locator X.fillableField preds++select :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+select locator preds =+ mkQuery "select" locator X.select preds++checkbox :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+checkbox locator preds =+ mkQuery "checkbox" locator X.checkbox preds++radioButton :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+radioButton locator preds =+ mkQuery "radioButton" locator X.radioButton preds++fileField :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+fileField locator preds =+ mkQuery "fileField" locator X.fileField preds++optgroup :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+optgroup locator preds =+ mkQuery "optgroup" locator X.optgroup preds++option :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+option locator preds =+ mkQuery "option" locator X.option preds++table :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+table locator preds =+ mkQuery "table" locator X.table preds++definitionDescription :: (Driver sess) => Text -> [Node sess -> HappybaraT sess m Bool] -> SimpleQuery sess m+definitionDescription locator preds =+ mkQuery "definitionDescription" locator (const . X.definitionDescription) preds++-- predicates++href :: (Driver sess, MonadBase IO m) => Text -> Node sess -> HappybaraT sess m Bool+href url node = do+ driver <- M.getDriver+ liftBase $ do+ (not . null) <$> D.findXPathRel driver node xpath+ where+ xpath = T.concat ["./self::*[./@href = ", X.stringLiteral url, "]"]++checked :: (Driver sess, MonadBase IO m) => Bool -> Node sess -> HappybaraT sess m Bool+checked b node = do+ driver <- M.getDriver+ liftBase $ do+ (b==) <$> D.isChecked driver node++unchecked :: (Driver sess, MonadBase IO m) => Bool -> Node sess -> HappybaraT sess m Bool+unchecked b node = do+ driver <- M.getDriver+ liftBase $ do+ (b/=) <$> D.isChecked driver node++disabled :: (Driver sess, MonadBase IO m) => Bool -> Node sess -> HappybaraT sess m Bool+disabled b node = do+ driver <- M.getDriver+ liftBase $ do+ name <- D.tagName driver node+ if name == "a"+ then return True+ else (b==) <$> D.isDisabled driver node++selected :: (Driver sess, MonadBase IO m) => NodeValue -> Node sess -> HappybaraT sess m Bool+selected (SingleValue val) node = do+ driver <- M.getDriver+ liftBase $ do+ ((SingleValue val) ==) <$> D.getValue driver node+selected (MultiValue vals) node = do+ driver <- M.getDriver+ liftBase $ do+ opts <- D.findXPathRel driver node ".//option"+ seld <- filterM (D.isSelected driver) opts+ texts <- mapM (D.visibleText driver) seld+ return $ sort vals == sort texts++options :: (Driver sess, MonadBase IO m) => [Text] -> Node sess -> HappybaraT sess m Bool+options opts node = do+ driver <- M.getDriver+ liftBase $ do+ options <- D.findXPathRel driver node ".//option"+ actual <- mapM (D.visibleText driver) options+ return $ (sort opts) == (sort actual)++elemType :: (Driver sess, MonadBase IO m) => Text -> Node sess -> HappybaraT sess m Bool+elemType t node = do+ driver <- M.getDriver+ liftBase $ do+ if any (t==) ["textarea", "select"]+ then (t==) <$> D.tagName driver node+ else (Just t==) <$> D.attr driver node "type"
+ src/Happybara/XPath.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE OverloadedStrings #-}++-- This module is generated from the Ruby 'xpath' gem.+-- See: https://gist.github.com/cstrahan/10015991++-- |+-- Copyright : (c) Charles Strahan 2014+-- License : MIT+-- Maintainer: Charles Strahan <charles.c.strahan@gmail.com>+-- Stability : experimental+--+-- This module provides XPath constructors for common HTML queries.+-- The 'Text' argument is the locator (e.g. id, type, href, etc), and the 'Bool'+-- argument indicates whether the generated XPath should match exactly or+-- inexactly.+--+-- XPath string literals can be properly quoted and escaped using+-- 'stringLiteral'.+--+-- /Note:/ These functions are mostly meant for internal use;+-- you probably want to use the queries in "Happybara.Query".+module Happybara.XPath where+import Data.Text as T++normalizeSpace :: Text -> Text+normalizeSpace = T.unwords . T.words++stringLiteral :: Text -> Text+stringLiteral str =+ go $ splitOn "'" str+ where+ go (x:[]) = T.concat ["'", x, "'"]+ go (xs) = T.concat ["concat('", T.intercalate "',\"'\",'" xs, "')"]++link :: Text -> Bool -> Text+link locator exact =+ T.concat $ if exact+ then [".//a[./@href][(((./@id = ", locatorLiteral, " or normalize-space(string(.)) = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ") or .//img[./@alt = ", locatorLiteral, "])]"]+ else [".//a[./@href][(((./@id = ", locatorLiteral, " or contains(normalize-space(string(.)), ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, ")) or .//img[contains(./@alt, ", locatorLiteral, ")])]"]+ where+ locatorLiteral = stringLiteral locator++button :: Text -> Bool -> Text+button locator exact =+ T.concat $ if exact+ then [".//input[./@type = 'submit' or ./@type = 'reset' or ./@type = 'image' or ./@type = 'button'][((./@id = ", locatorLiteral, " or ./@value = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ")] | .//input[./@type = 'image'][./@alt = ", locatorLiteral, "] | .//button[(((./@id = ", locatorLiteral, " or ./@value = ", locatorLiteral, ") or normalize-space(string(.)) = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ")] | .//input[./@type = 'image'][./@alt = ", locatorLiteral, "]"]+ else [".//input[./@type = 'submit' or ./@type = 'reset' or ./@type = 'image' or ./@type = 'button'][((./@id = ", locatorLiteral, " or contains(./@value, ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, "))] | .//input[./@type = 'image'][contains(./@alt, ", locatorLiteral, ")] | .//button[(((./@id = ", locatorLiteral, " or contains(./@value, ", locatorLiteral, ")) or contains(normalize-space(string(.)), ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, "))] | .//input[./@type = 'image'][contains(./@alt, ", locatorLiteral, ")]"]+ where+ locatorLiteral = stringLiteral locator++linkOrButton :: Text -> Bool -> Text+linkOrButton locator exact =+ T.concat $ if exact+ then [".//a[./@href][(((./@id = ", locatorLiteral, " or normalize-space(string(.)) = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ") or .//img[./@alt = ", locatorLiteral, "])] | .//input[./@type = 'submit' or ./@type = 'reset' or ./@type = 'image' or ./@type = 'button'][((./@id = ", locatorLiteral, " or ./@value = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ")] | .//input[./@type = 'image'][./@alt = ", locatorLiteral, "] | .//button[(((./@id = ", locatorLiteral, " or ./@value = ", locatorLiteral, ") or normalize-space(string(.)) = ", locatorLiteral, ") or ./@title = ", locatorLiteral, ")] | .//input[./@type = 'image'][./@alt = ", locatorLiteral, "]"]+ else [".//a[./@href][(((./@id = ", locatorLiteral, " or contains(normalize-space(string(.)), ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, ")) or .//img[contains(./@alt, ", locatorLiteral, ")])] | .//input[./@type = 'submit' or ./@type = 'reset' or ./@type = 'image' or ./@type = 'button'][((./@id = ", locatorLiteral, " or contains(./@value, ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, "))] | .//input[./@type = 'image'][contains(./@alt, ", locatorLiteral, ")] | .//button[(((./@id = ", locatorLiteral, " or contains(./@value, ", locatorLiteral, ")) or contains(normalize-space(string(.)), ", locatorLiteral, ")) or contains(./@title, ", locatorLiteral, "))] | .//input[./@type = 'image'][contains(./@alt, ", locatorLiteral, ")]"]+ where+ locatorLiteral = stringLiteral locator++fieldset :: Text -> Bool -> Text+fieldset locator exact =+ T.concat $ if exact+ then [".//fieldset[(./@id = ", locatorLiteral, " or ./legend[normalize-space(string(.)) = ", locatorLiteral, "])]"]+ else [".//fieldset[(./@id = ", locatorLiteral, " or ./legend[contains(normalize-space(string(.)), ", locatorLiteral, ")])]"]+ where+ locatorLiteral = stringLiteral locator++field :: Text -> Bool -> Text+field locator exact =+ T.concat $ if exact+ then [".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]"]+ else [".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]"]+ where+ locatorLiteral = stringLiteral locator++fillableField :: Text -> Bool -> Text+fillableField locator exact =+ T.concat $ if exact+ then [".//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]"]+ else [".//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//*[self::input | self::textarea][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'radio' or ./@type = 'checkbox' or ./@type = 'hidden' or ./@type = 'file')]"]+ where+ locatorLiteral = stringLiteral locator++select :: Text -> Bool -> Text+select locator exact =+ T.concat $ if exact+ then [".//select[(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//select"]+ else [".//select[(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//select"]+ where+ locatorLiteral = stringLiteral locator++checkbox :: Text -> Bool -> Text+checkbox locator exact =+ T.concat $ if exact+ then [".//input[./@type = 'checkbox'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//input[./@type = 'checkbox']"]+ else [".//input[./@type = 'checkbox'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//input[./@type = 'checkbox']"]+ where+ locatorLiteral = stringLiteral locator++radioButton :: Text -> Bool -> Text+radioButton locator exact =+ T.concat $ if exact+ then [".//input[./@type = 'radio'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//input[./@type = 'radio']"]+ else [".//input[./@type = 'radio'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//input[./@type = 'radio']"]+ where+ locatorLiteral = stringLiteral locator++fileField :: Text -> Bool -> Text+fileField locator exact =+ T.concat $ if exact+ then [".//input[./@type = 'file'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[normalize-space(string(.)) = ", locatorLiteral, "]/@for)] | .//label[normalize-space(string(.)) = ", locatorLiteral, "]//.//input[./@type = 'file']"]+ else [".//input[./@type = 'file'][(((./@id = ", locatorLiteral, " or ./@name = ", locatorLiteral, ") or ./@placeholder = ", locatorLiteral, ") or ./@id = //label[contains(normalize-space(string(.)), ", locatorLiteral, ")]/@for)] | .//label[contains(normalize-space(string(.)), ", locatorLiteral, ")]//.//input[./@type = 'file']"]+ where+ locatorLiteral = stringLiteral locator++optgroup :: Text -> Bool -> Text+optgroup locator exact =+ T.concat $ if exact+ then [".//optgroup[./@label = ", locatorLiteral, "]"]+ else [".//optgroup[contains(./@label, ", locatorLiteral, ")]"]+ where+ locatorLiteral = stringLiteral locator++option :: Text -> Bool -> Text+option locator exact =+ T.concat $ if exact+ then [".//option[normalize-space(string(.)) = ", locatorLiteral, "]"]+ else [".//option[contains(normalize-space(string(.)), ", locatorLiteral, ")]"]+ where+ locatorLiteral = stringLiteral locator++table :: Text -> Bool -> Text+table locator exact =+ T.concat $ if exact+ then [".//table[(./@id = ", locatorLiteral, " or .//caption = ", locatorLiteral, ")]"]+ else [".//table[(./@id = ", locatorLiteral, " or contains(.//caption, ", locatorLiteral, "))]"]+ where+ locatorLiteral = stringLiteral locator++definitionDescription :: Text -> Text+definitionDescription locator =+ T.concat [".//dd[(./@id = ", locatorLiteral, " or ./preceding-sibling::*[1]/self::dt[normalize-space(string(.)) = ", locatorLiteral, "])]"]+ where+ locatorLiteral = stringLiteral locator