hspec-webdriver 0.2.3 → 0.3.0
raw patch · 4 files changed
+197/−72 lines, 4 filesdep ~hspecdep ~webdriverPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: hspec, webdriver
API changes (from Hackage documentation)
- Test.Hspec.WebDriver: instance Example WDExample
- Test.Hspec.WebDriver: instance Typeable WdState
- Test.Hspec.WebDriver: sessionOn :: TestCapabilities cap => String -> Word16 -> String -> String -> ([cap], Spec) -> Spec
+ Test.Hspec.WebDriver: instance Typeable multi => Example (WDExample multi)
+ Test.Hspec.WebDriver: instance Typeable1 WdState
+ Test.Hspec.WebDriver: multiSession :: (TestCapabilities cap, Typeable a, Eq a) => a -> String -> ([cap], Spec) -> Spec
+ Test.Hspec.WebDriver: multiSessionWith :: (TestCapabilities cap, Typeable a, Eq a) => WDConfig -> a -> String -> ([cap], Spec) -> Spec
+ Test.Hspec.WebDriver: runIO :: IO a -> SpecM a
+ Test.Hspec.WebDriver: runWDWith :: (Eq a, Typeable a) => a -> WD () -> WDExample a
+ Test.Hspec.WebDriver: sessionWith :: TestCapabilities cap => WDConfig -> String -> ([cap], Spec) -> Spec
- Test.Hspec.WebDriver: data WDExample
+ Test.Hspec.WebDriver: data WDExample multi
- Test.Hspec.WebDriver: newCaps :: TestCapabilities c => c -> WD Capabilities
+ Test.Hspec.WebDriver: newCaps :: TestCapabilities c => c -> IO Capabilities
- Test.Hspec.WebDriver: runWD :: WD () -> WDExample
+ Test.Hspec.WebDriver: runWD :: WD () -> WDExample ()
Files
- Changelog.md +29/−0
- Test/Hspec/WebDriver.hs +132/−49
- Test/Hspec/WebDriver/Internal.hs +32/−20
- hspec-webdriver.cabal +4/−3
+ Changelog.md view
@@ -0,0 +1,29 @@+## 0.3.0++* Add support for testing multiple sessions at once+ * multiSession, multiSessionWith, and runWDWith are the new functions+ * the type of runWD and WDExample changed++* Update to hspec 0.11 and webdriver 0.6+ * changed sessionOn to sessionWith to work with new webdriver WDConfig+ * TestCapabilities(newCaps) changed type to @c -> IO W.Capabilities@++## 0.2.3++* Add inspectSession to assist debugging the test suite++## 0.2.2++* Allow newer version of hspec++## 0.2.1++* Allow newer version of hspec++## 0.2.0++* Convert to use webdriver sessions++## 0.1.0++* Initial Release
Test/Hspec/WebDriver.hs view
@@ -36,12 +36,17 @@ -- * Webdriver BrowserDefaults(..) , session- , sessionOn+ , sessionWith , runWD- , WDExample- , Using(..) , inspectSession+ , Using(..) + -- * Multiple sessions at once+ , multiSession+ , multiSessionWith+ , runWDWith+ , WDExample+ -- * Expectations , shouldBe , shouldBeTag@@ -62,6 +67,7 @@ , parallel , pending , pendingWith+ , runIO -- * Re-exports from "Test.WebDriver" , WD@@ -70,12 +76,10 @@ ) where import Control.Exception.Lifted (try, Exception, onException, throwIO, catch)-import Control.Monad (when)+import Control.Monad (when, forM_) import Control.Monad.IO.Class (liftIO)-import Data.Default (def) import Data.IORef import Data.Typeable (Typeable)-import Data.Word (Word16) import Test.HUnit (assertEqual, assertFailure) import qualified Data.Text as T @@ -86,7 +90,8 @@ import Test.WebDriver (WD) import Test.WebDriver.Commands import qualified Test.WebDriver as W-import qualified Test.WebDriver.Classes as W+import qualified Test.WebDriver.Session as W+import qualified Test.WebDriver.Config as W import qualified Test.Hspec.WebDriver.Internal as I @@ -116,7 +121,7 @@ -- should be created. class Show c => TestCapabilities c where -- | The capabilities to pass to 'createSession'.- newCaps :: c -> WD W.Capabilities+ newCaps :: c -> IO W.Capabilities instance TestCapabilities BrowserDefaults where newCaps Firefox = return $ W.defaultCaps { W.browser = W.firefox }@@ -141,25 +146,89 @@ -- This function uses the default webdriver host (127.0.0.1), port (4444), and -- basepath (@\/wd\/hub@). session :: TestCapabilities cap => String -> ([cap], Spec) -> Spec-session = hSessionWd W.defaultSession+session = hSessionWd create+ where+ create :: TestCapabilities cap => cap -> IO (WdState ())+ create = createSt W.defaultConfig --- | A variation of 'session' which allows you to specify the webdriver host, port, and basepath.-sessionOn :: TestCapabilities cap- => String -- ^ host- -> Word16 -- ^ port- -> String -- ^ base path- -> String -- ^ message- -> ([cap], Spec)- -> Spec-sessionOn host port bp = hSessionWd def { W.wdHost = host- , W.wdPort = port- , W.wdBasePath = bp- }+-- | A variation of 'session' which allows you to specify the webdriver configuration. Note that+-- the capabilities in the 'W.WDConfig' will be ignored, instead the capabilities will come from the+-- list of 'TestCapabilities'.+sessionWith :: TestCapabilities cap+ => W.WDConfig -> String -> ([cap], Spec) -> Spec+sessionWith config = hSessionWd create+ where+ create :: TestCapabilities cap => cap -> IO (WdState ())+ create = createSt config --- | A typeclass of things which can be converted to a list of capabilities. It's primary purpose--- is to allow the word @using@ to be used with 'session' so that the session description reads like--- a sentance.+-- | Allows testing multiple browser sessions at once. --+-- The way this works is you create a type @a@ to index the sessions, pass an undefined value to+-- 'multiSession', and then use values of type @a@ with 'runWDWith' to identify which session the+-- example should run with. The first time 'runWDWith' sees a value, a new session is created. Note+-- that the examples are still run serially in depth-first order.+--+-- Note that in hspec1, the requirement that every example inside 'multiSession' must use 'runWDWith'+-- with the same type @a@ is not checked by types. In <http://hackage.haskell.org/package/hspec2 hspec2>+-- the types are expressive enough so that this can be checked by the type system (and also means+-- 'multiSession' does not need the undefined value of type @a@).+--+-- I use this for testing multiple users at once, with one user in each browser session.+--+-- >data TestUser = Gandolf | Bilbo | Legolas+-- > deriving (Show, Eq, Enum, Bounded, Typeable)+-- >+-- >usersSession :: TestCapabilities cap => String -> ([cap],Spec) -> Spec+-- >usersSession = multiSession (undefined :: TestUser)+-- >+-- >runUser :: TestUser -> WD () -> WDExample TestUser+-- >runUser = runWDWith+-- >+-- >spec :: Spec+-- >spec = usersSession "tests some page" $ using Firefox $ do+-- > it "does something with Gandolf" $ runUser Gandolf $ do+-- > openPage ...+-- > it "does something with Bilbo" $ runUser Bilbo $ do+-- > openPage ...+-- > it "goes back to the Gandolf session" $ runUser Gandolf $ do+-- > e <- findElem ....+-- > ...+--+-- In the above code, two sessions are created and the examples will go back and forth between the+-- two sessions. Note that a session for Legolas will only be created the first time he shows up in+-- a call to @runUser@. To share information between the sessions (e.g. some data that Gandolf+-- creates that Bilbo should expect), the best way I have found is to use 'runIO' to create an+-- IORef while constructing the spec. Note this can be hidden inside the @usersSession@ function.+multiSession :: (TestCapabilities cap, Typeable a, Eq a)+ => a -- ^ Can be an undefined value of type a, this is used only to determine the type+ -> String -- ^ the message+ -> ([cap], Spec) -- ^ the list of capabilites and the spec+ -> Spec+multiSession val = hSessionWd $ create val+ where+ create :: (Typeable a, Eq a, TestCapabilities cap) => a -> cap -> IO (WdState a)+ create _ = createSt W.defaultConfig++-- | A variation of 'multiSession' which allows you to specify the webdriver configuration. Note that+-- the capabilities in the 'W.WDConfig' will be ignored, instead the capabilities will come from the+-- list of 'TestCapabilities'.+multiSessionWith :: (TestCapabilities cap, Typeable a, Eq a)+ => W.WDConfig+ -> a -- ^ Can be an undefined value of type a, this is used only to determine the type+ -> String -- ^ the message+ -> ([cap], Spec) -- ^ the list of capabilites and the spec+ -> Spec+multiSessionWith config val = hSessionWd $ create val+ where+ create :: (Typeable a, Eq a, TestCapabilities cap) => a -> cap -> IO (WdState a)+ create _ = createSt config++-- | A typeclass of things which can be converted to a list of capabilities. It has two uses.+-- First, it allows you to create a datatype of grouped capabilities in addition to your actual+-- capabilities. These psudo-caps can be passed to @using@ to convert them to a list of your actual+-- capabilities. Secondly, it allows the word @using@ to be used with 'session' so that the session+-- description reads like a sentance.+-- -- >session "for the home page" $ using Firefox $ do -- > it "loads the page" $ runWD $ do -- > ...@@ -230,8 +299,9 @@ -------------------------------------------------------------------------------- -- | State passed between examples-data WdState = WdState {- stSession :: W.WDSession -- ^ the webdriver session+data WdState a = WdState {+ stInitialConfig :: W.WDConfig -- ^ initial config+ , stSessions :: [(a,W.WDSession)] -- ^ the webdriver sessions , stError :: IORef Bool -- ^ has an error occured in an earlier example? We rely on the serialization -- of examples to ensure that at most one thread is reading/writing this -- ioref.@@ -242,47 +312,60 @@ deriving (Show, Typeable) instance Exception PrevHasError --- | The initial session is used only for its host, port, and basepath. A new session is created.-createSt :: TestCapabilities cap => W.WDSession -> cap -> IO WdState-createSt sess cap = do+-- | Create the internal WdState+createSt :: TestCapabilities cap => W.WDConfig -> cap -> IO (WdState a)+createSt cfg cap = do err <- newIORef False- sess' <- W.runWD sess $ newCaps cap >>= createSession- return $ WdState sess' err+ caps <- newCaps cap+ return $ WdState cfg { W.wdCapabilities = caps} [] err -closeSt :: WdState -> IO ()-closeSt st = W.runWD (stSession st) closeSession+closeSt :: WdState a -> IO ()+closeSt st = + forM_ (stSessions st) $ \(_, sess) ->+ W.runWD sess closeSession -- | The 'WDSession' passed in is used for its host, port, and base path.-hSessionWd :: TestCapabilities cap => W.WDSession -> String -> ([cap], Spec) -> Spec-hSessionWd sess msg (caps, spec) = spec'+hSessionWd :: (Typeable a, TestCapabilities cap) => (cap -> IO (WdState a)) -> String -> ([cap], Spec) -> Spec+hSessionWd create msg (caps, spec) = spec' where spec' = case caps of [] -> it msg $ pendingWith "No capabilities specified" [c] -> describe (msg ++ " using " ++ show c) $ proc c spec _ -> describe msg $ mapM_ (\c -> describe ("using " ++ show c) $ proc c spec) caps - proc cap = mapSpecItem addCatchResult . I.session (createSt sess cap) closeSt+ proc cap = mapSpecItem addCatchResult . I.session (create cap) closeSt addCatchResult item = item {-#if MIN_VERSION_hspec(1,10,0) itemExample = \p a prog -> itemExample item p a prog-#else- itemExample = \p a -> itemExample item p a-#endif `catch` \PrevHasError -> return $ Pending $ Just "previous example had error" } -- | An example that can be passed to 'it' containing a webdriver action. It must be created with--- 'runWD'.-newtype WDExample = WdExample (I.SessionExample WdState)+-- 'runWD' or 'runWDWith'.+newtype WDExample multi = WdExample (I.SessionExample (WdState multi)) deriving Example -- | Create an example from a 'WD' action. This /must/ be nested inside a call to 'session' or--- 'sessionOn'.-runWD :: WD () -> WDExample-runWD w = WdExample $ I.SessionExample $ \s -> W.runWD (stSession s) $ do- err <- liftIO $ readIORef $ stError s+-- 'sessionWith'.+runWD :: WD () -> WDExample ()+runWD = runWDWith ()++-- | Create an example from a 'WD' action, parameterized by which session to run.+-- This /must/ be nested inside a call to 'multiSession' or 'multiSessionWith' and can only be used+-- when multiple sessions are running. Also, the type @a@ must match the type given to+-- 'multiSession'.+runWDWith :: (Eq a, Typeable a) => a -> WD () -> WDExample a+runWDWith a w = WdExample $ I.SessionExample $ \state -> do+ err <- readIORef $ stError state when err $ throwIO PrevHasError - w `onException` liftIO (writeIORef (stError s) True)- swd <- W.getSession- return s { stSession = swd }+ sess <- case lookup a (stSessions state) of+ Just s -> return s+ Nothing -> do+ let cfg = stInitialConfig state+ s <- W.mkSession cfg+ W.runWD s $ W.createSession $ W.wdCapabilities cfg++ W.runWD sess $ do+ w `onException` liftIO (writeIORef (stError state) True)+ swd <- W.getSession+ return state { stSessions = (a,swd) : filter ((/=a) . fst) (stSessions state) }
Test/Hspec/WebDriver/Internal.hs view
@@ -14,36 +14,45 @@ import Data.Traversable (traverse) import Data.Typeable (Typeable, cast) import Data.IORef-import System.IO.Unsafe (unsafePerformIO) import Test.Hspec-#if MIN_VERSION_hspec(1,10,0) import Test.Hspec.Core hiding (describe, it)-#else-import Test.Hspec.Core hiding (describe, it, hspec)-#endif import qualified Control.Exception as E -#if MIN_VERSION_hspec(1,10,0)+-- | Force a spec tree to not contain any BuildSpecs+forceSpecTree :: SpecTree -> IO [SpecTree]+forceSpecTree s@(SpecItem _ _) = return [s]+forceSpecTree (SpecGroup msg ss) = do+ ss' <- concat <$> mapM forceSpecTree ss+ return [SpecGroup msg ss']+forceSpecTree (BuildSpecs m) = do+ trees <- m+ concat <$> mapM forceSpecTree trees++-- | Force a spec to not contain any BuildSpecs+forceSpec :: Spec -> IO [SpecTree]+forceSpec s = do+ trees <- runSpecM s+ concat <$> mapM forceSpecTree trees++-- | Traverse a spec, but only if forceSpec has already been called traverseTree :: Applicative f => (Item -> f Item) -> SpecTree -> f SpecTree traverseTree f (SpecItem msg i) = SpecItem msg <$> f i traverseTree f (SpecGroup msg ss) = SpecGroup msg <$> traverse (traverseTree f) ss-#else-traverseTree :: Applicative f => (Item -> f Item) -> SpecTree -> f SpecTree-traverseTree f (SpecItem i) = SpecItem <$> f i-traverseTree f (SpecGroup msg ss) = SpecGroup msg <$> traverse (traverseTree f) ss-#endif+traverseTree _ (BuildSpecs _) = error "No BuildSpecs should be left" -traverseSpec :: Applicative f => (Item -> f Item) -> Spec -> f Spec-traverseSpec f s = fromSpecList <$> traverse (traverseTree f) (runSpecM s)+-- | Traverse a list of specs, but only if forceSpecs has already been called+traverseSpec :: Applicative f => (Item -> f Item) -> [SpecTree] -> f [SpecTree]+traverseSpec f trees = traverse (traverseTree f) trees + -- | Process the items in a depth-first walk, passing in the item counter value.-mapWithCounter :: (Int -> Item -> Item) -> Spec -> Spec+mapWithCounter :: (Int -> Item -> Item) -> [SpecTree] -> [SpecTree] mapWithCounter f s = flip evalState 0 $ traverseSpec go s where go item = state $ \cnt -> (f cnt item, cnt+1) -countItems :: Spec -> Int+countItems :: [SpecTree] -> Int countItems s = flip execState 0 $ traverseSpec go s where go item = state $ \cnt -> (item, cnt+1)@@ -155,11 +164,14 @@ -> (s -> IO ()) -- ^ cleanup the state -> Spec -- ^ spec tree to process -> Spec-session create close s = unsafePerformIO $ do- let cnt = countItems s- mvars <- sequence $ take cnt $ repeat newEmptyMVar- let sess = Session cnt mvars create close- return $ mapWithCounter (sessionItem sess) s+session create close s = fromSpecList [build]+ where+ build = BuildSpecs $ do+ trees <- forceSpec s+ let cnt = countItems trees+ mvars <- sequence $ take cnt $ repeat newEmptyMVar+ let sess = Session cnt mvars create close+ return (mapWithCounter (sessionItem sess) trees) -- | Create an example to pass to 'it' which accesses and modifies the state using the state monad. -- For example,
hspec-webdriver.cabal view
@@ -1,5 +1,5 @@ name: hspec-webdriver-version: 0.2.3+version: 0.3.0 cabal-version: >= 1.8 build-type: Simple synopsis: Write end2end web application tests using webdriver and hspec@@ -16,6 +16,7 @@ with <http://hspec.github.io hspec>. extra-source-files: README.md+ Changelog.md source-repository head type: mercurial@@ -31,11 +32,11 @@ , HUnit >= 1.2 && < 1.3 , hashable >= 1.2- , hspec >= 1.8+ , hspec >= 1.11 , data-default >= 0.5 , lifted-base >= 0.2 , stm >= 2.4 , text >= 0.11 , transformers >= 0.3 , unordered-containers >= 0.2- , webdriver >= 0.5 && < 0.6+ , webdriver >= 0.6 && < 0.7