webdriver 0.1 → 0.2
raw patch · 10 files changed
+233/−101 lines, 10 files
Files
- CHANGELOG.md +24/−0
- TODO +33/−0
- Test/WebDriver/Commands.hs +52/−15
- Test/WebDriver/Commands/Internal.hs +2/−2
- Test/WebDriver/Commands/Wait.hs +11/−5
- Test/WebDriver/Firefox/Profile.hs +8/−8
- Test/WebDriver/Internal.hs +1/−1
- Test/WebDriver/JSON.hs +1/−1
- Test/WebDriver/Types.hs +92/−53
- webdriver.cabal +9/−16
+ CHANGELOG.md view
@@ -0,0 +1,24 @@+#Change Log++## hs-webdriver 0.2++### API changes+* FailedCommandInfo changed so that it stores a WDSession rather than just a Maybe SessionId, thus providing server host and port information as well as the session ID.+* As a result, mkFailedcommandInfo is now String -> WD FailedCommandInfo, since it requires access to the WDSession state.+* HTML5StorageType changed to the more accurate WebStorageType++### new features+* general documentation improvements+* the uploadFile, uploadRawFile, and uploadZipEntry functions, which support uploading file contents to the remote server++## hs-webdriver 0.1++### API changes+* getWindowSize, setWindowSize, getWindowPos, and setWindowPos have all been deprived of their WindowHandle argument. This is due to the fact that using unfocused windows with those commands causes undefined behavior. ++### new features+* the mkCookie function for convenient cookie construction+* the focusFrame function for focusing to individual frames+* the setPageLoadTimeout function+* the maximize function for maximizing windows+* support for HTML 5 web storage
+ TODO view
@@ -0,0 +1,33 @@+for next minor releases++-- add special web-driver specific FF profile setters, either as special+functions or via documentation++-- add opera config++-- improve documentation. Document errors better. Provide more+module introductions+ -- provide examples+ -- basic runSession usage+ -- intermediate usage+ -- explicit waits usage+ -- firefox profile usage+ -- REPL usage+++for 1.0++-- overload WD commands in typeclass+-- add WDSettings, use WDSession as internal State type. +-- allow settings to automatically load drivers. add modules with driver loading+functions+-- allow setting to modify HTTP headers++++miscellaneous unsorted++-- use http-conduit and attoparsec-conduit to stream JSON data. use request methods from http-types++-- use ReaderT to store WDVersion for use with implicit configurations.+
Test/WebDriver/Commands.hs view
@@ -42,17 +42,22 @@ , moveTo, moveToCenter, moveToFrom , clickWith, MouseButton(..) , mouseDown, mouseUp, withMouseDown, doubleClick- -- * Touch gestures- , touchClick, touchDown, touchUp, touchMove- , touchScroll, touchScrollFrom, touchDoubleClick- , touchLongClick, touchFlick, touchFlickFrom -- * Screen orientation , Orientation(..) , getOrientation, setOrientation -- * Geo-location , getLocation, setLocation- -- * HTML 5 web storage+ -- * HTML 5 Web Storage , storageSize, getAllKeys, deleteAllKeys, getKey, setKey, deleteKey+ -- * Uploading files to remote server+ -- |These functions allow you to upload a file to a remote server. + -- Note that this operation isn't supported by all WebDriver servers,+ -- and the location where the file is stored is not standardized.+ , uploadFile, uploadRawFile, uploadZipEntry+ -- * Touch gestures+ , touchClick, touchDown, touchUp, touchMove+ , touchScroll, touchScrollFrom, touchDoubleClick+ , touchLongClick, touchFlick, touchFlickFrom -- * IME support , availableIMEEngines, activeIMEEngine, checkIMEActive , activateIME, deactivateIME@@ -67,9 +72,11 @@ import Data.Aeson import qualified Data.Text as T import Data.Text (Text, splitOn, append)-import Data.ByteString (ByteString)+import Data.ByteString as SBS (ByteString, concat) import Data.ByteString.Base64 as B64+import Data.ByteString.Lazy as LBS (ByteString, toChunks) import Network.URI+import Codec.Archive.Zip import Control.Applicative import Control.Monad.State.Strict@@ -122,7 +129,7 @@ allFields = ["type" .= ("implicit" :: String)] ++ msField -- |Sets the amount of time we wait for an asynchronous script to return a --- result+-- result. setScriptTimeout :: Integer -> WD () setScriptTimeout ms = doSessCommand POST "/timeouts/async_script" (object msField)@@ -131,6 +138,7 @@ where msField = ["ms" .= ms] allFields = ["type" .= ("script" :: String)] ++ msField +-- |Sets the amount of time to wait for a page to finish loading before throwing a 'Timeout' exception setPageLoadTimeout :: Integer -> WD () setPageLoadTimeout ms = doSessCommand POST "/timeouts" params where params = object ["type" .= ("page load" :: String)@@ -195,7 +203,7 @@ timeout err = throwIO err -- |Grab a screenshot of the current page as a PNG image-screenshot :: WD ByteString+screenshot :: WD SBS.ByteString screenshot = B64.decodeLenient <$> doSessCommand GET "/screenshot" () @@ -509,20 +517,49 @@ "longitude", "altitude") -storageSize :: HTML5StorageType -> WD Integer+-- |Uploads a file from the local filesystem by its file path.+uploadFile :: FilePath -> WD ()+uploadFile path = uploadZipEntry =<< liftIO (readEntry [] path)+ +-- |Uploads a raw bytestring with associated file info.+uploadRawFile :: FilePath -- ^File path to use with this bytestring.+ -> Integer -- ^Modification time + -- (in seconds since Unix epoch).+ -> LBS.ByteString -- ^ The file contents as a lazy ByteString+ -> WD ()+uploadRawFile path t str = uploadZipEntry (toEntry path t str)+++-- |Lowest level interface to the file uploading mechanism.+-- This allows you to specify the exact details of+-- the zip entry sent across network.+uploadZipEntry :: Entry -> WD ()+uploadZipEntry = doSessCommand POST "/file" . single "file" + . B64.encode . SBS.concat . toChunks + . fromArchive . (`addEntryToArchive` emptyArchive)++-- |Get the current number of keys in a web storage area.+storageSize :: WebStorageType -> WD Integer storageSize s = doStorageCommand GET s "/size" () -getAllKeys :: HTML5StorageType -> WD [Text]+-- |Get a list of all keys from a web storage area.+getAllKeys :: WebStorageType -> WD [Text] getAllKeys s = doStorageCommand GET s "" () -deleteAllKeys :: HTML5StorageType -> WD ()+-- |Delete all keys within a given web storage area.+deleteAllKeys :: WebStorageType -> WD () deleteAllKeys s = doStorageCommand DELETE s "" () -getKey :: HTML5StorageType -> Text -> WD Text+-- |Get the value associated with a key in the given web storage area.+-- Unset keys result in empty strings, since the Web Storage spec+-- makes no distinction between the empty string and an undefined value.+getKey :: WebStorageType -> Text -> WD Text getKey s k = doStorageCommand GET s ("/key/" `T.append` k) () -setKey :: HTML5StorageType -> Text -> Text -> WD Text+-- |Set a key in the given web storage area.+setKey :: WebStorageType -> Text -> Text -> WD Text setKey s k v = doStorageCommand POST s "" . object $ ["key" .= k,- "value" .= v ] -deleteKey :: HTML5StorageType -> Text -> WD ()+ "value" .= v ]+-- |Delete a key in the given web storage area. +deleteKey :: WebStorageType -> Text -> WD () deleteKey s k = doStorageCommand POST s ("/key/" `T.append` k) ()
Test/WebDriver/Commands/Internal.hs view
@@ -31,7 +31,7 @@ doWinCommand = doWinCommand' [] doStorageCommand :: (ToJSON a, FromJSON b) =>- RequestMethod -> HTML5StorageType -> Text -> a -> WD b+ RequestMethod -> WebStorageType -> Text -> a -> WD b doStorageCommand = doStorageCommand' [] doCommand' :: (ToJSON a, FromJSON b) => @@ -67,7 +67,7 @@ doSessCommand' h m (T.concat ["/element/", e, path]) a doStorageCommand' :: (ToJSON a, FromJSON b) =>- [Header] -> RequestMethod -> HTML5StorageType -> Text -> a+ [Header] -> RequestMethod -> WebStorageType -> Text -> a -> WD b doStorageCommand' h m s path a = doSessCommand' h m (T.concat ["/", s', path]) a where s' = case s of
Test/WebDriver/Commands/Wait.hs view
@@ -20,10 +20,10 @@ import Prelude hiding (catch) instance Exception ExpectFailed--- |An exception representing a failure of an expected condition.+-- |An exception representing the failure of an expected condition. data ExpectFailed = ExpectFailed deriving (Show, Eq, Typeable) --- |Raises ExpectFailed.+-- |throws 'ExpectFailed'. This is nice for writing your own abstractions. unexpected :: WD a unexpected = throwIO ExpectFailed @@ -44,15 +44,21 @@ (<||>) :: Monad m => m Bool -> m Bool -> m Bool (<||>) = liftM2 (||) ++-- |Apply a predicate to every element in a list, and expect that at least one+-- succeeds. expectAny :: (a -> WD Bool) -> [a] -> WD () expectAny p xs = expect . or =<< mapM p xs +-- |Apply a predicate to every element in a list, and expect that all succeed. expectAll :: (a -> WD Bool) -> [a] -> WD () expectAll p xs = expect . and =<< mapM p xs -- |Wait until either the given action succeeds or the timeout is reached. -- The action will be retried every .25 seconds until no ExpectFailed or--- NoSuchElement exceptions occur. The timeout value is expressed in seconds.+-- NoSuchElement exceptions occur. If the timeout is reached, then a +-- 'Test.WebDriver.Timeout' exception will be raised. The timeout value is +-- expressed in seconds. waitUntil :: Double -> WD a -> WD a waitUntil = waitUntil' 250000 @@ -70,12 +76,12 @@ handleExpectFailed (_ :: ExpectFailed) = retry --- |Like waitWhile, but retries the action until it fails or until the timeout+-- |Like 'waitUntil', but retries the action until it fails or until the timeout -- is exceeded. waitWhile :: Double -> WD a -> WD () waitWhile = waitWhile' 250000 --- |Like waitWhile', but retries the action until it either fails or +-- |Like 'waitUntil\'', but retries the action until it either fails or -- until the timeout is exceeded. waitWhile' :: Int -> Double -> WD a -> WD () waitWhile' = wait' handler
Test/WebDriver/Firefox/Profile.hs view
@@ -65,7 +65,13 @@ } deriving (Eq, Show) --- |A Firefox preference. This is the subset of JSON values that excludes++-- |Represents a Firefox profile that has been prepared for +-- network transmission. The profile cannot be modified in this form.+newtype PreparedFirefoxProfile = PreparedFirefoxProfile ByteString+ deriving (Eq, Show, ToJSON, FromJSON)++-- |A Firefox preference value. This is the subset of JSON values that excludes -- arrays and objects. data FirefoxPref = PrefInteger !Integer | PrefDouble !Double@@ -81,12 +87,7 @@ PrefBool b -> toJSON b --- |Represents a Firefox profile that has been prepared for --- network transmission. The profile cannot be modified in this form.-newtype PreparedFirefoxProfile = PreparedFirefoxProfile ByteString- deriving (Eq, Show, ToJSON, FromJSON) - instance Exception ProfileParseError -- |An error occured while attempting to parse a profile's prefs.js file newtype ProfileParseError = ProfileParseError String@@ -191,7 +192,6 @@ $ parseOnly prefsParser s -- |Prepare a FirefoxProfile for network transmission.--- -- Internally, this function constructs a Firefox profile within a temp -- directory, archives it as a zip file, and then base64 encodes the zipped -- data. The temporary directory is deleted afterwards@@ -228,7 +228,7 @@ -- |Apply a function on an automatically generated default profile, and -- prepare the result. The FirefoxProfile passed to the handler function is--- the same one used by sessions when no Firefox profile is specified+-- the default profile used by sessions when Nothing is specified prepareTempProfile :: MonadIO m => (FirefoxProfile -> FirefoxProfile) -> m PreparedFirefoxProfile
Test/WebDriver/Internal.hs view
@@ -104,7 +104,7 @@ sess <- get errInfo <- fromJSON' val let screen = B64.decodeLenient <$> errScreen errInfo - errInfo' = errInfo { errSessId = wdSessId sess + errInfo' = errInfo { errSess = sess , errScreen = screen } e errType = throwIO $ FailedCommand errType errInfo' case status of
Test/WebDriver/JSON.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}--- |A collection of convenience functions for using and parsing JSON valus+-- |A collection of convenience functions for using and parsing JSON values -- within 'WD'. All monadic parse errors are converted to asynchronous -- 'BadJSON' exceptions. module Test.WebDriver.JSON
Test/WebDriver/Types.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, TemplateHaskell, OverloadedStrings, ExistentialQuantification, - MultiParamTypeClasses, TypeFamilies, NoMonoLocalBinds #-}+ MultiParamTypeClasses, TypeFamilies, NoMonoLocalBinds + #-} {-# OPTIONS_HADDOCK not-home #-} module Test.WebDriver.Types ( -- * WebDriver sessions@@ -9,7 +10,10 @@ , Capabilities(..), defaultCaps, allCaps , Platform(..), ProxyType(..) -- ** Browser-specific configuration- , Browser(..), firefox, chrome, ie, opera, iPhone, iPad, android+ , Browser(..), + -- ** Default settings for browsers+ firefox, chrome, ie, opera, iPhone, iPad, android+ , FFLogPref -- * WebDriver objects and command-specific types , Element(..) , WindowHandle(..), currentWindow@@ -19,7 +23,7 @@ , Cookie(..), mkCookie , Orientation(..) , MouseButton(..)- , HTML5StorageType(..)+ , WebStorageType(..) -- * Exceptions , InvalidURL(..), NoSessionId(..), BadJSON(..) , HTTPStatusUnknown(..), HTTPConnError(..)@@ -38,7 +42,7 @@ import Network.Stream (ConnError) -import Data.Text as Text (toLower, toUpper)+import Data.Text as Text (toLower, toUpper, unpack) import Data.Text (Text) import Data.ByteString (ByteString) @@ -48,14 +52,16 @@ import Control.Monad.State.Strict import Control.Monad.Base import Control.Monad.Trans.Control+import Data.Maybe import Data.Word import Data.String+import Text.Show import Data.Default import qualified Data.Char as C {- |A monadic interface to the WebDriver server. This monad is a simple, strict -wrapper over 'IO', threading session information between sequential commands+layer over 'IO', threading session information between sequential commands -} newtype WD a = WD (StateT WDSession IO a) deriving (Functor, Monad, MonadState WDSession, MonadIO@@ -112,11 +118,14 @@ wdHost :: String -- |Port number of the server , wdPort :: Word16- -- |An opaque reference identifying the session.+ -- |An opaque reference identifying the session to+ -- use with 'WD' commands. -- A value of Nothing indicates that a session - -- hasn't been created yet. To create new sessions,- -- use the 'Test.WebDriver.Commands.createSession' - -- and 'Test.WebDriver.runSession' functions.+ -- hasn't been created yet.+ -- Sessions can be created within 'WD' via the+ -- 'Test.WebDriver.createSession', or created+ -- and closed automatically with + -- 'Test.WebDriver.runSession' , wdSessId :: Maybe SessionId } deriving (Eq, Show) @@ -139,7 +148,7 @@ it's created. In this usage, fields that are set to Nothing indicate that we have no preference for that capability. -* When returned by 'Test.WebDriver.Commands.getCaps', it's used to+* When received from the server , it's used to describe the actual capabilities given to us by the WebDriver server. Here a value of Nothing indicates that the server doesn't support the capability. Thus, for Maybe Bool fields, both Nothing and@@ -189,8 +198,8 @@ } -- |Default capabilities. This is the same as the 'Default' instance, but with --- a more specific type. By default we use Firefox of an unspecified version --- with default settings on whatever platform is available. +-- less polymorphism. By default, we use 'firefox' of an unspecified 'version' +-- with default system-wide proxy settings on whatever 'platform' is available. -- All Maybe Bool capabilities are set to Nothing (no preference). defaultCaps :: Capabilities defaultCaps = def@@ -214,7 +223,7 @@ -- |Browser setting and browser-specific capabilities.-data Browser = Firefox { -- |The firefox profile to use. If Nothing, a+data Browser = Firefox { -- |The firefox profile to use. If Nothing, -- a default temporary profile is automatically created -- and used. ffProfile :: Maybe PreparedFirefoxProfile@@ -224,7 +233,10 @@ -- system-based default. , ffBinary :: Maybe FilePath }- | Chrome { chromeDriverVersion :: Maybe String + | Chrome { -- |Version of the Chrome Webdriver server server to use.+ -- for more information on chromedriver see+ -- <http://code.google.com/p/selenium/wiki/ChromeDriver>+ chromeDriverVersion :: Maybe String -- |Path to Chrome binary. If Nothing, use a sensible -- system-based default. , chromeBinary :: Maybe FilePath@@ -237,7 +249,7 @@ | IE { ignoreProtectedModeSettings :: Bool --, useLegacyInternalServer :: Bool }- | Opera -- ^ Opera-specific configuration coming soon!+ | Opera -- ^ (Opera-specific configuration coming soon!) | HTMLUnit | IPhone | IPad@@ -296,7 +308,7 @@ deriving (Eq, Show) --- |For Firefox sessions; indicates Firefox's log level+-- |For 'Firefox' sessions; indicates Firefox's log level data FFLogPref = LogOff | LogSevere | LogWarning | LogInfo | LogConfig | LogFine | LogFiner | LogFinest | LogAll deriving (Eq, Show, Ord, Bounded, Enum)@@ -339,8 +351,8 @@ deriving (Eq, Show, Typeable) instance Exception FailedCommand--- |This exception encapsulates many different kinds of exceptions that can--- occur when a command fails. +-- |This exception encapsulates a broad variety of exceptions that can+-- occur when a command fails. data FailedCommand = FailedCommand FailedCommandType FailedCommandInfo deriving (Eq, Show, Typeable) @@ -373,31 +385,35 @@ deriving (Eq, Ord, Enum, Bounded, Show) -- |Detailed information about the failed command provided by the server.-data FailedCommandInfo = FailedCommandInfo { errMsg :: String- , errSessId :: Maybe SessionId +data FailedCommandInfo = FailedCommandInfo { -- |The error message.+ errMsg :: String+ -- |The session associated with + -- the exception.+ , errSess :: WDSession + -- |A screen shot of the focused window+ -- when the exception occured,+ -- if provided. , errScreen :: Maybe ByteString+ -- |The "class" in which the exception+ -- was raised, if provided. , errClass :: Maybe String+ -- |A stack trace of the exception. , errStack :: [StackFrame] } deriving (Eq) -- |Constructs a FailedCommandInfo from only an error message.-mkFailedCommandInfo :: String -> FailedCommandInfo-mkFailedCommandInfo m = FailedCommandInfo {errMsg = m- , errSessId = Nothing- , errScreen = Nothing- , errClass = Nothing- , errStack = []- }+mkFailedCommandInfo :: String -> WD FailedCommandInfo+mkFailedCommandInfo m = do+ sess <- get+ return $ FailedCommandInfo {errMsg = m , errSess = sess , errScreen = Nothing+ , errClass = Nothing , errStack = [] } -- |Convenience function to throw a 'FailedCommand' locally with no server-side -- info present. failedCommand :: FailedCommandType -> String -> WD a-failedCommand t m = throwIO . FailedCommand t =<< getCmdInfo- where getCmdInfo = do- sessId <- wdSessId <$> get- return $ (mkFailedCommandInfo m) { errSessId = sessId }+failedCommand t m = throwIO . FailedCommand t =<< mkFailedCommandInfo m -- |An individual stack frame from the stack trace provided by the server -- during a FailedCommand.@@ -406,15 +422,24 @@ , sfMethodName :: String , sfLineNumber :: Word }- deriving (Show, Eq)+ deriving (Eq) --- |Cookies are delicious delicacies.+-- |Cookies are delicious delicacies. When sending cookies to the server, a value+-- of Nothing indicates that the server should use a default value. When receiving+-- cookies from the server, a value of Nothing indicates that the server is unable+-- to specify the value. data Cookie = Cookie { cookName :: Text- , cookValue :: Text- , cookPath :: Maybe Text- , cookDomain :: Maybe Text- , cookSecure :: Maybe Bool- , cookExpiry :: Maybe Integer+ , cookValue :: Text -- ^ + , cookPath :: Maybe Text -- ^path of this cookie.+ -- if Nothing, defaults to /+ , cookDomain :: Maybe Text -- ^domain of this cookie.+ -- if Nothing, the current pages+ -- domain is used+ , cookSecure :: Maybe Bool -- ^Is this cookie secure?+ , cookExpiry :: Maybe Integer -- ^Expiry date expressed as+ -- seconds since the Unix epoch+ -- Nothing indicates that the + -- cookie never expires } deriving (Eq, Show) -- |Creates a Cookie with only a name and value specified. All other@@ -429,7 +454,7 @@ data Selector = ById Text | ByName Text | ByClass Text -- ^ (Note: multiple classes are not - -- allowed. For more control, use ByCSS)+ -- allowed. For more control, use 'ByCSS') | ByTag Text | ByLinkText Text | ByPartialLinkText Text@@ -450,22 +475,36 @@ deriving (Eq, Show, Ord, Bounded, Enum) --- |A type to specify HTML 5 storage type-data HTML5StorageType = LocalStorage | SessionStorage - deriving (Eq, Show, Ord, Bounded, Enum)+-- |An HTML 5 storage type+data WebStorageType = LocalStorage | SessionStorage + deriving (Eq, Show, Ord, Bounded, Enum) instance Show FailedCommandInfo where --todo: pretty print- show i = showString "{errMsg = " . shows (errMsg i) - . showString ", errSessId = " . shows (errSessId i)- . showString ", errScreen = " . screen- . showString ", errClass = " . shows (errClass i)- . showString ", errStack = " . shows (errStack i) - $ "}"- where screen = showString $ case errScreen i of - Just _ -> "Just \"...\""- Nothing -> "Nothing"- + show i = showChar '\n' + . showString "Session: " . sess + . showChar '\n'+ . showString className . showString ": " . showString (errMsg i)+ . showChar '\n'+ . foldl (\f s-> f . showString " " . shows s) id (errStack i)+ $ ""+ where+ className = fromMaybe "<unknown exception>" . errClass $ i+ + sess = showString sessId . showString " at " + . showString host . showChar ':' . shows port+ where+ WDSession {wdHost = host, wdPort = port, wdSessId = msid } = errSess i+ sessId = case msid of+ Just (SessionId sid) -> unpack sid+ Nothing -> "<no session id>" +instance Show StackFrame where+ show f = showString (sfClassName f) . showChar '.' + . showString (sfMethodName f) . showChar ' '+ . showParen True ( showString (sfFileName f) . showChar ':'+ . shows (sfLineNumber f))+ $ "\n"+ instance FromJSON Element where parseJSON (Object o) = Element <$> o .: "ELEMENT" parseJSON v = typeMismatch "Element" v@@ -544,7 +583,7 @@ instance FromJSON FailedCommandInfo where parseJSON (Object o) = FailedCommandInfo <$> (req "message" >>= maybe (return "") return)- <*> pure Nothing+ <*> pure undefined <*> opt "screen" Nothing <*> opt "class" Nothing <*> opt "stackTrace" []
webdriver.cabal view
@@ -1,5 +1,5 @@ Name: webdriver-Version: 0.1+Version: 0.2 Cabal-Version: >= 1.6 License: BSD3 License-File: LICENSE@@ -9,30 +9,25 @@ Category: Web, Browser, Testing Synopsis: a Haskell client for the Selenium WebDriver protocol Build-type: Simple-Extra-source-files: README.md+Extra-source-files: README.md, TODO, CHANGELOG.md Description:- A Selenium WebDriver client for the Haskell- programming language. You can use it to automate browser- sessions for testing, system administration, etc.-+ A Selenium WebDriver client for Haskell. + You can use it to automate browser sessions + for testing, system administration, etc.+ . For more information about Selenium itself, see <http://seleniumhq.org/>-+ . To find out what's been changed in this version and others,- see the changelog at + see the change log at <https://github.com/kallisti-dev/hs-webdriver/blob/master/CHANGELOG.md> --source-repository this- type: git- location: git://github.com/kallisti-dev/hs-webdriver.git- tag: 0- source-repository head type: git location: git://github.com/kallisti-dev/hs-webdriver.git library+ ghc-options: -Wall build-depends: base == 4.* , aeson >= 0.4 && < 0.7 , HTTP >= 4000.1 && < 4000.3@@ -67,5 +62,3 @@ other-modules: Test.WebDriver.Internal Test.WebDriver.Types.Internal-- ghc-options: -Wall