packages feed

hspec-webdriver (empty) → 0.1.0

raw patch · 6 files changed

+499/−0 lines, 6 filesdep +HUnitdep +basedep +hashablesetup-changed

Dependencies added: HUnit, base, hashable, hspec, lifted-base, stm, text, transformers, unordered-containers, webdriver

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2013 John Lenz++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.
+ README.md view
@@ -0,0 +1,5 @@+This package integrates [hspec](http://hspec.github.io) with+[hs-webdriver](https://hackage.haskell.org/package/webdriver), allowing you to write webdriver tests+using hspec.  This package contains no code testing the `hspec-webdriver` package itself.  The+`webdriver-angular` package contains some test code which test both the Angular webdriver commands+in `webdriver-angular` and some tests of functions in `hspec-webdriver`. 
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Test/Hspec/WebDriver.hs view
@@ -0,0 +1,219 @@+{-# LANGUAGE OverloadedStrings, FlexibleInstances, DeriveDataTypeable, TypeFamilies #-}+-- | Write hspec tests that are webdriver tests, automatically managing the webdriver sessions.+--+-- This module re-exports functions from "Test.Hspec" and "Test.WebDriver.Commands" and it is+-- intended that you just import @Test.Hspec.WebDriver@.  If you need to import @Test.Hspec@ or+-- @Test.WebDriver@, you should do so using a qualified import.+--+-- >{-# LANGUAGE OverloadedStrings #-}+-- >module XKCD where+-- >+-- >import Test.Hspec.WebDriver+-- >+-- >main :: IO ()+-- >main = hspec $+-- >    describe "XKCD Tests" $ do+-- >        it "checks hover text of 327" $ using Firefox $ do+-- >            openPage "http://www.xkcd.com/327/"+-- >            e <- findElem $ ByCSS "div#comic > img"+-- >            e `shouldBeTag` "img"+-- >            e `shouldHaveAttr` ("title", "Her daughter is named Help I'm trapped in a driver's license factory.")+-- >+-- >        parallel $ it "checks title of 303" $ using [Firefox, Chrome] $ do+-- >            openPage "http://www.xkcd.com/303/"+-- >            e <- findElem $ ById "ctitle"+-- >            e `shouldBeTag` "div"+-- >            e `shouldHaveText` "Compiling"+-- >+-- >        it "checks image of 1310" pending+--+-- The above code assumes selenium-server-standalone is running on @127.0.0.1:4444@ at path+-- @\/wd\/hub@ (this is the default).  You can configure this using `createSessionManager'`. +module Test.Hspec.WebDriver(+  -- * Webdriver+    BrowserDefaults(..)+  , it+  , Using(..)+  , pending+  , pendingWith+  , WdExpectation(..)++  -- * Expectations+  , shouldBe+  , shouldBeTag+  , shouldHaveText+  , shouldHaveAttr+  , shouldReturn+  , shouldThrow++  -- * Session Manager+  , createSessionManager+  , createSessionManager'++  -- * Custom Capabilities+  , TestCapabilities(..)++  -- * Re-exports from hspec+  , hspec+  , Spec+  , describe+  , context+  , parallel+  -- $beforeTodo++  -- * Re-exports from "Test.WebDriver"+  , WD+  , liftIO+  , module Test.WebDriver.Commands++  -- * Internal+  , withCaps+) where++import Control.Exception.Lifted (try, Exception)+import Control.Monad.IO.Class (liftIO)+import Data.Typeable (Typeable)+import Test.HUnit (assertEqual, assertFailure)+import Test.Hspec hiding (shouldReturn, shouldBe, shouldSatisfy, shouldThrow, it, pending, pendingWith)+import Test.Hspec.Core (Result(..), fromSpecList, SpecTree(..), Item(..), Params)+import Test.WebDriver hiding (Browser(..))+import Test.WebDriver.Commands+import qualified Test.WebDriver as W+import qualified Test.Hspec as H+import qualified Data.Text as T++import Test.Hspec.WebDriver.Internal++-- | Webdriver expectations consist of a set of browser 'Capabilities' to use and the actual test as+-- a 'W.WD' monad.  The browser capabilities are specified by an enumeration which is an instance of+-- 'TestCapabilities'.  The @BrowserDefaults@ enumeration provides items that represent the default set of+-- capabilities for each browser.  When creating new sessions, the 'defaultCaps' are used.  Also,+-- any existing session (which exists at program startup) which matches the browser is used, no+-- matter the actual capabilities.+--+-- To obtain more control over the capabilities (e.g. to test multiple versions of IE or to test+-- Firefrox without javascript), you should @import Test.Hspec.WebDriver hiding (BrowserDefaults)@+-- and then create your own enumeration which is an instance of 'TestCapabilities' and 'Using'.+data BrowserDefaults = Firefox | Chrome | IE | Opera | IPhone | IPad | Android+    deriving (Eq, Show, Enum, Bounded, Typeable)++instance TestCapabilities BrowserDefaults where+    matchesCaps Firefox (Capabilities { browser = W.Firefox _ _ _ }) = True+    matchesCaps Chrome (Capabilities { browser = W.Chrome _ _ _ _ }) = True+    matchesCaps IE (Capabilities { browser = W.IE _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ }) = True+    matchesCaps Opera (Capabilities { browser = W.Opera _ _ _ _ _ _ _ _ _ _ _ _ }) = True+    matchesCaps IPhone (Capabilities { browser = W.IPhone}) = True+    matchesCaps IPad (Capabilities { browser = W.IPad }) = True+    matchesCaps Android (Capabilities { browser = W.Android }) = True+    matchesCaps _ _ = False++    newCaps Firefox = return $ defaultCaps { browser = firefox }+    newCaps Chrome = return $ defaultCaps { browser = chrome }+    newCaps IE = return $ defaultCaps { browser = ie }+    newCaps Opera = return $ defaultCaps { browser = opera }+    newCaps IPhone = return $ defaultCaps { browser = iPhone }+    newCaps IPad = return $ defaultCaps { browser = iPad }+    newCaps Android = return $ defaultCaps { browser = android }++-- | A webdriver expectation is either an action and a list of capabilities (which should be an instance of+-- 'TestCapabilities') or a pending message.+data WdExpectation cap = WdTest [cap] (WD ())+                       | WdPending (Maybe String)++-- | Evaluate an expectation to a list of the function expected by hspec.+evaluateWd :: (Show cap, TestCapabilities cap) => WdExpectation cap -> [(String,Params -> (IO () -> IO ()) -> IO Result)]+evaluateWd (WdPending msg) = [("", \_ _ -> return $ Pending msg)]+evaluateWd (WdTest cs test) = map mkItem cs+    where+        mkItem c = ("using " ++ show c, eval c)++        eval :: TestCapabilities c => c -> params -> (IO () -> IO ()) -> IO Result+        eval c _ action = do action (runWD defaultSession $ withCaps c test)+                             return Success++-- | A typeclass of things which can be converted to a 'WdExpectation'.  This is the primary method+-- to create expectations to pass to 'it'.  Both a single 'BrowserDefaults' and a list+-- of 'BrowserDefaults' can be used.+--+-- >it "opens the home page" $ using Firefox $ do+-- >    ...+-- >it "opens the users page" $ using [Firefox, Chrome] $ do+-- >    ...+class Using a where+    type UsingCapabilities a :: *+    using :: a -> WD () -> WdExpectation (UsingCapabilities a)++instance Using BrowserDefaults where+    type UsingCapabilities BrowserDefaults = BrowserDefaults+    using d = WdTest [d]++instance Using [BrowserDefaults] where+    type UsingCapabilities [BrowserDefaults] = BrowserDefaults+    using = WdTest++-- | Create a spec from a webdriver expectation.+--+-- The webdriver expectation consists of a list of browser capabilities and the actual test.  The+-- test will be executed once for each set of capabilities in the list.  By default, the tests will be+-- executed serially.  You can use 'parallel' to execute the test on all capabilities in parallel.+--+-- To run the test, a webdriver session is allocated from a pool of sessions (in a thread-safe+-- manner).  The pools will be initialized automatically from existing sessions the first time a+-- test is run; you can explicitly create the pools using 'createSessionManager'.+it :: (Show cap, TestCapabilities cap) => String -> WdExpectation cap -> Spec+it msg a = spec+    where+        mkItem m f = SpecItem Item { itemIsParallelizable = False+                                   , itemRequirement = m+                                   , itemExample = f+                                   }+        spec = case evaluateWd a of+                [] -> fromSpecList []+                [("",f)] -> fromSpecList [mkItem (msg) f]+                [(m,f)] -> fromSpecList [mkItem (msg ++ " " ++ m) f]+                ss -> describe msg $ fromSpecList $ map (uncurry mkItem) ss++-- | A 'WdExpectation' that is pending.+pending :: WdExpectation ()+pending = WdPending Nothing++-- | A 'WdExpectation' that is pending, with a message.+pendingWith :: String -> WdExpectation ()+pendingWith = WdPending . Just++-- | 'H.shouldBe' lifted into the 'WD' monad.+shouldBe :: (Show a, Eq a) => a -> a -> WD ()+x `shouldBe` y = liftIO $ x `H.shouldBe` y++-- | Asserts that the given element matches the given tag.+shouldBeTag :: Element -> T.Text -> WD ()+e `shouldBeTag` name = do+    t <- tagName e+    liftIO $ assertEqual ("tag of " ++ show e) name t++-- | Asserts that the given element has the given text.+shouldHaveText :: Element -> T.Text -> WD ()+e `shouldHaveText` txt = do+    t <- getText e+    liftIO $ assertEqual ("text of " ++ show e) txt t++-- | Asserts that the given elemnt has the attribute given by @(attr name, value)@.+shouldHaveAttr :: Element -> (T.Text, T.Text) -> WD ()+e `shouldHaveAttr` (a, txt) = do+    t <- attr e a+    liftIO $ assertEqual ("attribute " ++ T.unpack a ++ " of " ++ show e) (Just txt) t++-- | Asserts that the action returns the expected result.+shouldReturn :: (Show a, Eq a) => WD a -> a -> WD ()+action `shouldReturn` expected = action >>= (\a -> liftIO $ a `H.shouldBe` expected)++-- | Asserts that the action throws an exception.+shouldThrow :: (Show e, Eq e, Exception e) => WD a -> e -> WD ()+shouldThrow w expected = do+    r <- try w+    case r of+        Left err -> err `shouldBe` expected+        Right _ -> liftIO $ assertFailure $ "did not get expected exception " ++ show expected++-- $beforeTodo+-- Future TODO: lift 'before', 'after', and 'around' into the 'WD' monad.
+ Test/Hspec/WebDriver/Internal.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE ExistentialQuantification #-}+module Test.Hspec.WebDriver.Internal(+    TestCapabilities(..)+  , createSessionManager+  , createSessionManager'+  , withCaps+) where++import Control.Concurrent.STM+import Control.Exception.Lifted (finally, onException)+import Control.Monad.IO.Class (liftIO)+import Data.Hashable+import Data.List (partition)+import Data.Maybe (fromJust)+import Data.Typeable+import Data.Word (Word16)+import System.IO.Unsafe (unsafePerformIO)+import Test.WebDriver+import Test.WebDriver.Classes+import qualified Data.HashMap.Lazy as M++-- | Provides information about the browser capabilities used for testing.  If you want more control+-- over capabilities, you should hide @BrowserDefaults@ and then make+-- an enumeration of all the webdriver capabilities you will be testing with.  For example,+--+-- >data TestCaps = Firefox+-- >              | FirefoxWithoutJavascript+-- >              | Chrome+-- >              | IE8+-- >              | IE9+-- >   deriving (Show, Eq, Bounded, Enum, Typeable)+--+-- @TestCaps@ must then be made an instance of @TestCapabilities@.  Also, instances of @Using@+-- should be created.+class (Eq c, Enum c, Typeable c) => TestCapabilities c where++    -- | Check if the 'Capabilities' match your enumeration.  Note that these capabilities+    -- will be the actual capabilities (with things like version information filled in)+    -- so you should not use @==@ to compare capabilities, only check the actual capabilities you+    -- care about.+    matchesCaps :: c -> Capabilities -> Bool++    -- | The capabilities to pass to 'createSession' when no existing session is found.+    newCaps :: c -> WD Capabilities++-- | This instance is used for pending messages, no capabilities are matched or created.+instance TestCapabilities () where+    matchesCaps () _ = False+    newCaps () = error "Cannot create caps for ()"++data SomeCap = forall c. TestCapabilities c => SomeCap c++instance Eq SomeCap where+    SomeCap c1 == SomeCap c2 = case cast c2 of+                                Just c2' -> c1 == c2'+                                Nothing -> False++instance Hashable SomeCap where+    hashWithSalt i (SomeCap c) = hashUsing fromEnum i c++data ManagedSessions = ManagedSessions+  { maxSessions :: Int+    -- ^ maximum number of sessions that should be created+  , managedSessions :: M.HashMap SomeCap (TVar ([SessionId],Int))+    -- ^ For each cap, store the list of unused sessions and a count of in-use sessions.+  , initialSessions :: [(SessionId,Capabilities)]+    -- ^ sessions which existed at program start and are not yet assigned.++  -- | settings for session+  , mwdHost :: String+  , mwdPort :: Word16+  , mwdBasePath :: String+  }++-- | Stores the managed sessions+sessionManager :: TVar (Maybe ManagedSessions)+{-# NOINLINE sessionManager #-}+sessionManager = unsafePerformIO (newTVarIO Nothing)++-- | Create and set the session manager+createWdMan :: Int -> Maybe (String, Word16, String) -> WD ()+createWdMan maxSess mSettings = do+    let (host,port,bpath) = case mSettings of+            Just s -> s+            Nothing -> (wdHost defaultSession, wdPort defaultSession, wdBasePath defaultSession)+    sess <- sessions++    let manager = ManagedSessions maxSess M.empty sess host port bpath++    liftIO $ atomically $ do+        mm <- readTVar sessionManager+        case mm of+            Just _ -> return () -- manager has already been created+            Nothing -> writeTVar sessionManager $ Just manager++-- | Create a new session manager using the default webdriver host (127.0.0.1), port (4444), and+-- basepath (@\/wd\/hub@).+--+-- The session manager hands out sessions to tests (in a thread-safe manner).  Threads ask for+-- sessions by an enumeration which is an instance of 'TestCapabilities', and the manager stores a+-- pool of sessions for each enumeration item.  When calling 'createSessionManager', the already+-- existing sessions are loaded and used as the initial sessions in the pools.  If a thread asks for+-- a session but none is available, one of two things happens:  if the total number of sessions for+-- this enumeration item is larger than the argument to 'createSessionManager', the thread will+-- block until a session is available.  If the total number of sessions for this enumeration item is+-- smaller, a new session will be created.  This is only relevant if you run tests in parallel,+-- since when running tests serially at most one session will be in use at any one time in any case.+-- Note that sessions are never closed by the manager.+--+-- If you do not call 'createSessionManager', when the very first test is run a new manager will be+-- created where the maximum number of sessions per enumeration item is one.+createSessionManager :: Int -- ^ threshold number of sessions per enumeration item beyond which new +                            -- sessions are no longer created.  Note you can set this to zero so+                            -- that new sessions are never created; the only sessions used will be+                            -- those that already exist.+                     -> IO ()+createSessionManager maxSess = runWD defaultSession $ createWdMan maxSess Nothing++-- | Same as 'createSessionManager' but allows you to specify the webdriver host, port, and base+-- path for all sessions.+createSessionManager' :: Int  -- ^ threshold number of sessions per enumeration item+                      -> String -- ^ host+                      -> Word16 -- ^ port+                      -> String -- ^ base path+                      -> IO ()+createSessionManager' maxSess host port bpath = do+    let sess = WDSession { wdHost = host+                         , wdPort = port+                         , wdBasePath = bpath+                         , wdSessId = Nothing+                         , lastHTTPRequest = Nothing+                         }+    runWD sess $ createWdMan maxSess $ Just (host, port, bpath)++-- | Create a session+createSessionId :: SomeCap -> WD SessionId+createSessionId (SomeCap c) = do+    caps <- newCaps c+    sess <- createSession caps+    return $ fromJust $ wdSessId sess++-- | Searches for a session. Returns the action to be used to obtain the session.+findSession :: SomeCap -> ManagedSessions -> STM (WD SessionId)+findSession sc@(SomeCap c) m =+    case M.lookup sc $ managedSessions m of+        Just tvar -> do+            (sess,count) <- readTVar tvar+            case sess of+                (s:ss) -> do writeTVar tvar (ss, count + 1)+                             return $ return s++                [] | count >= maxSessions m -> retry -- retry blocks until a tvar changes++                   | otherwise -> do writeTVar tvar ([], count + 1)+                                     let create = createSessionId sc `onException` (liftIO $ atomically $ do+                                                    (s',cnt) <- readTVar tvar+                                                    writeTVar tvar (s', cnt - 1))+                                     return create++        Nothing -> do+            let (sess, unmanaged') = partition (\(_,cap) -> matchesCaps c cap) $ initialSessions m+            tvar <- newTVar (map fst sess, 0)+            let m' = m { initialSessions = unmanaged'+                       , managedSessions = M.insert sc tvar $ managedSessions m+                       }+            writeTVar sessionManager $ Just m'+            findSession sc m'++-- | Take a session out of the pool, using an existing unused session, creating a new session, or+-- blocking if the maximum number of sessions already exist.  The new session is set into the 'WD'+-- monad.  Note the session can leak if you do not properly call 'putSessionId'.+takeSession :: TestCapabilities s => s -> WD ()+takeSession s = do+    msess <- liftIO $ atomically $ do+        mm <- readTVar sessionManager+        case mm of+            Nothing -> return Nothing+            Just m -> do r <- findSession (SomeCap s) m+                         return $ Just (r, mwdHost m, mwdPort m, mwdBasePath m)+    case msess of+        Just (r, host, port, bpath) -> do+            let sess = WDSession { wdHost = host+                                 , wdPort = port+                                 , wdBasePath = bpath+                                 , wdSessId = Nothing+                                 , lastHTTPRequest = Nothing+                                 }+            putSession sess+            sid <- r+            putSession sess { wdSessId = Just sid }++        Nothing -> do+            createWdMan 1 Nothing+            takeSession s++-- | Add a session ID back into the pool.+putSessionId :: TestCapabilities s => s -> SessionId -> WD ()+putSessionId s sid = liftIO $ atomically $ do+    mm <- readTVar sessionManager+    let m = maybe (error "Cannot put a session to an uninitialized manager") id mm+    case M.lookup (SomeCap s) $ managedSessions m of+        Nothing -> error "Cannot put a session to a cap that does not exist"+        Just tvar -> do+            (ss,cnt) <- readTVar tvar+            writeTVar tvar (sid:ss,cnt-1)++-- | Find or create a new session, set it into the 'WD' monad, run the given action, and return the+-- session back into the pool once the action completes or an exception occurs.+withCaps :: TestCapabilities s => s -> WD a -> WD a+withCaps tc test = do+    takeSession tc+    sess <- getSession+    test `finally` putSessionId tc (fromJust $ wdSessId sess)
+ hspec-webdriver.cabal view
@@ -0,0 +1,40 @@+name:              hspec-webdriver+version:           0.1.0+cabal-version:     >= 1.8+build-type:        Simple+synopsis:          Write end2end web application tests using webdriver and hspec+category:          Web+author:            John Lenz <lenz@math.uic.edu>+maintainer:        John Lenz <lenz@math.uic.edu>+license:           MIT+license-file:      LICENSE+homepage:          https://bitbucket.org/wuzzeb/webdriver-utils+stability:         Experimental+description:       For end to end testing of web applications from Haskell, the +                   <https://hackage.haskell.org/package/webdriver webdriver> package is a great tool but just+                   contains the code to communicate with the browser.  This package integrates webdriver+                   with <http://hspec.github.io hspec>.++extra-source-files: README.md++source-repository head+    type: mercurial+    location: https://bitbucket.org/wuzzeb/webdriver-utils++library+    hs-source-dirs:  .+    exposed-modules:  Test.Hspec.WebDriver+    other-modules: Test.Hspec.WebDriver.Internal+    ghc-options:   -Wall -O2++    build-depends: base                 >= 4          && < 5++                 , HUnit                >= 1.2 && < 1.3+                 , hashable             >= 1.2+                 , hspec                >= 1.8 && < 1.9+                 , lifted-base          >= 0.2+                 , stm                  >= 2.4+                 , text                 >= 0.11+                 , transformers         >= 0.3+                 , unordered-containers >= 0.2+                 , webdriver            >= 0.5  && < 0.6