diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/happybara.cabal b/happybara.cabal
new file mode 100644
--- /dev/null
+++ b/happybara.cabal
@@ -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
diff --git a/src/Happybara.hs b/src/Happybara.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara.hs
@@ -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
diff --git a/src/Happybara/Driver.hs b/src/Happybara/Driver.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara/Driver.hs
@@ -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 <==>
diff --git a/src/Happybara/Exceptions.hs b/src/Happybara/Exceptions.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara/Exceptions.hs
@@ -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
diff --git a/src/Happybara/Monad.hs b/src/Happybara/Monad.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara/Monad.hs
@@ -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
diff --git a/src/Happybara/Query.hs b/src/Happybara/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara/Query.hs
@@ -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"
diff --git a/src/Happybara/XPath.hs b/src/Happybara/XPath.hs
new file mode 100644
--- /dev/null
+++ b/src/Happybara/XPath.hs
@@ -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
