packages feed

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