packages feed

webdriver-wrapper (empty) → 0.1.0.0

raw patch · 9 files changed

+427/−0 lines, 9 filesdep +aesondep +asyncdep +base

Dependencies added: aeson, async, base, bytestring, directory, extra, filepath, http-conduit, http-types, process, process-extras, retry, string-interpolate, tar, text, unix, unordered-containers, webdriver, zip-archive, zlib

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `webdriver-wrapper`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## Unreleased++## 0.1.0.0 - YYYY-MM-DD
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2024 Gabriel Tollini++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1.  Redistributions of source code must retain the above copyright notice, this+    list of conditions and the following disclaimer.++2.  Redistributions in binary form must reproduce the above copyright notice,+    this list of conditions and the following disclaimer in the documentation+    and/or other materials provided with the distribution.++3.  Neither the name of the copyright holder nor the names of its contributors+    may be used to endorse or promote products derived from this software+    without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,11 @@+# webdriver-wrapper++This package was created so end-users don't need to manually manage Selenium or their webdriver when dealing with the [webdriver](https://hackage.haskell.org/package/webdriver) package. For now, only `geckodriver` (aka Firefox) is supported - but `chromedriver` should be easy to implement. If you can't use Firefox for whichever reason, let me know and I'll look into implementing `chromedriver` support. ++This package is strongly inspired by [sandwich-webdriver](https://hackage.haskell.org/package/sandwich-webdriver). My original use-case didn't require the sandwich test suite, though, so I made this package which intends on being simpler and more minimalistic. ++## Extra dependencies+You must have Java installed, since Selenium is distributed as a `.jar` file. I'm running OpenJDK 17 on my machine, and it works perfectly with the selected Selenium version. ++## How to use+There are two functions which are "batteries-included": `wrappedRunSession` and `wrappedRunWD`. Just use them as you would `runSession` and `runWD`. The wrapped functions will handle downloading Selenium and geckodriver (if they were not already downloaded), plus starting and stopping Selenium. If you already have some code with `runSession` or `runWD`, their wrapped counterparts are drop-in replacements. 
+ src/Test/WebDriverWrapper.hs view
@@ -0,0 +1,41 @@+{- |+Module : Core+Description : end-user functions.++The wrapped functions (`wrappedRunSession` and `wrappedRunWD`) will download Selenium and Firefox's webdriver (geckodriver) if they're not already on the `Test.WebDriverWrapper.Constants.downloadPath`, then start Selenium before running the webdriver equivalent function (`runSession` and `runWD`). They kill the Selenium process at the end of their execution. +-}+module Test.WebDriverWrapper (wrappedRunSession, wrappedRunWD, wrapWebDriverFunction, downloadIfMissing) where++import System.Process (cleanupProcess)+import Control.Concurrent.Async (concurrently_)+import Test.WebDriverWrapper.Selenium (getSeleniumIfNeeded, startSelenium)+import Test.WebDriverWrapper.GeckoDriver (getGeckoDriverIfNeeded)+import Test.WebDriver (runSession)+import Test.WebDriver.Types (WD, WDSession)+import Test.WebDriver.Config (WebDriverConfig)+import Test.WebDriver.Monad (runWD)+import Control.Exception (bracket)++-- | Same as `runSession`, but starts Selenium before execution and kills Selenium after execution. +-- Will download Selenium or Firefox's webdriver (geckodriver) if any is missing. +wrappedRunSession :: WebDriverConfig conf => conf -> WD a -> IO a+wrappedRunSession conf wd = wrapWebDriverFunction (conf,wd) (uncurry runSession) ++-- | Same as `runWD`, but starts Selenium before execution and kills Selenium after execution. +-- Will download Selenium or Firefox's webdriver (geckodriver) if any is missing. +wrappedRunWD ::  WDSession -> WD a -> IO a +wrappedRunWD session wd = wrapWebDriverFunction (session, wd) (uncurry runWD)++-- | Runs a function in between starting and killing Selenium. Takes in the arguments and the function, in that order. +-- Will download Selenium or Firefox's webdriver (geckodriver) if any is missing. +wrapWebDriverFunction :: a -> (a -> IO b) -> IO b+wrapWebDriverFunction webdriverArgs webdriverFunction = do+    downloadIfMissing+    bracket+        startSelenium+        cleanupProcess+        (const $ webdriverFunction webdriverArgs)++-- | Dowloads Selenium or Firefox's webdriver (geckodriver) if they're missing. +downloadIfMissing :: IO()+downloadIfMissing =  concurrently_ getSeleniumIfNeeded getGeckoDriverIfNeeded
+ src/Test/WebDriverWrapper/Constants.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++{- |+Module : Test.WebDriverWrapper.Constants+Description : Constant values, such as links and paths.+ -}++module Test.WebDriverWrapper.Constants (geckoDriverPath, defaultPath, defaultSeleniumJarUrl, desiredPlatform, getGeckoDriverDownloadUrl, geckoDriverVersionSource, downloadPath, geckoArchivePath, fileFormat, seleniumPath, seleniumLogPath) where++import Data.String.Interpolate (i)+import qualified System.Info as SI+import System.Directory (getXdgDirectory, XdgDirectory (XdgData))+import System.FilePath ((</>))++-- Paths. Should be platform independent.+-- | @haskell-webdriver-wrapper@ directory, created at `XdgData`. +defaultPath :: IO FilePath+defaultPath = getXdgDirectory XdgData "haskell-webdriver-wrapper"++-- | Directory named after `desiredPlatform`, created at the `defaultPath`.+downloadPath :: IO FilePath+downloadPath = (</> desiredPlatform) <$> defaultPath++-- | Intermediary path for the compressed version of geckodriver. Inside `downloadPath`.+geckoArchivePath :: IO FilePath+geckoArchivePath = (</> archive) <$> downloadPath+    where+        archive = "geckodriver" <> fileFormat++-- | Path for geckodriver. Inside `downloadPath`. +geckoDriverPath :: IO FilePath+geckoDriverPath = (</> "geckodriver") <$> downloadPath++-- | Path for selenium.jar. Inside `downloadPath`.+seleniumPath :: IO FilePath+seleniumPath = (</> "selenium.jar") <$> downloadPath++-- | Path for Selenium's log file. Inside `defaultPath`. +seleniumLogPath :: IO FilePath+seleniumLogPath = (</> "selenium.log") <$> defaultPath++-- URLs. Might change at any moment, kinda why I'm putting them all together here.+-- | Url to download Selenium from. +defaultSeleniumJarUrl :: String+defaultSeleniumJarUrl = "https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar"++-- | API to get geckodriver's latest version.+geckoDriverVersionSource :: String+geckoDriverVersionSource = "https://api.github.com/repos/mozilla/geckodriver/releases/latest"++-- | Url to download geckodriver from. Always the latest version provided by `geckoDriverVersionSource`.+getGeckoDriverDownloadUrl :: String -> String+getGeckoDriverDownloadUrl version = [i|https://github.com/mozilla/geckodriver/releases/download/#{version}/geckodriver-#{version}-#{platform}#{format}|]+    where+        platform = desiredPlatform+        format = fileFormat++-- Platform-dependent variables. +-- | Archive format for geckodriver's download. @.zip@ for Windows, @.tar.gz@ for everyone else. +fileFormat :: String+fileFormat = case SI.os of+    "windows" -> ".zip"+    "mingw32" -> ".zip"++    "darwrin" -> ".tar.gz"+    "linux"   -> ".tar.gz"++    _         -> ".tar.gz"++-- | Platform this code is running at. The options are:+-- +--      * win64+--      * win-aarch64+--      * win32+--      * macos+--      * macos-aarch64+--      * linux64+--      * linux-aarch64+--      * linux32+-- +--  If the platform is not identified, @linux64@ is used. +desiredPlatform :: String+desiredPlatform = case (SI.os, SI.arch) of+    ("windows", "x86_64")   -> "win64"+    ("windows", "aarch64")  -> "win-aarch64"+    ("windows", "i386")     -> "win32"+    ("mingw32", "x86_64")   -> "win64"+    ("mingw32", "aarch64")  -> "win-aarch64"+    ("mingw32", "i386")     -> "win32"++    ("darwin", "x86_64")    -> "macos"+    ("darwin", "aarch64")   -> "macos-aarch64"++    ("linux", "x86_64")     -> "linux64"+    ("linux", "aarch64")    -> "linux-aarch64"+    ("linux", "i386")       -> "linux32"++    _ -> "linux64"
+ src/Test/WebDriverWrapper/GeckoDriver.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Test.WebDriverWrapper.GeckoDriver (getGeckoDriverIfNeeded) where++import qualified Data.Text as T+import Network.HTTP.Simple (setRequestMethod, httpLBS, parseRequest, setRequestHeader)+import Test.WebDriverWrapper.Helpers (download, decompress)+import Test.WebDriverWrapper.Constants (getGeckoDriverDownloadUrl, geckoDriverVersionSource, geckoArchivePath, geckoDriverPath, downloadPath)+import Network.HTTP.Client.Conduit (Response(responseBody))+import Data.Aeson (eitherDecode)+import Network.HTTP.Types (hUserAgent)+import qualified Data.Aeson.KeyMap as AKM+import qualified Data.Aeson as A+import System.Directory (createDirectoryIfMissing, removeFile, doesFileExist)+import Control.Monad (unless)++-- | Checks if @geckodriver@ is in the `downloadPath`. If not, download it. +getGeckoDriverIfNeeded :: IO ()+getGeckoDriverIfNeeded = do+    geckoPath <- geckoDriverPath+    hasGeckoDriver  <- doesFileExist geckoPath+    unless hasGeckoDriver getGeckoDriver++getGeckoDriver :: IO()+getGeckoDriver = do+    version <- getGeckoDriverVersion+    dPath <- downloadPath+    tarballPath <- geckoArchivePath++    createDirectoryIfMissing True dPath++    let url = getGeckoDriverDownloadUrl version+    download url tarballPath+    decompress tarballPath dPath+    removeFile tarballPath+    +getGeckoDriverVersion :: IO String+getGeckoDriverVersion = do+    requestUrl <- parseRequest versionAPI+    let+        request+            = setRequestMethod "GET"+            $ setRequestHeader hUserAgent ["cli"]+            requestUrl+    response <- responseBody <$> httpLBS request+    let+        version' = eitherDecode response  :: Either String A.Object+        maybeVersion = case version' of+            (Left err) -> error err+            (Right version'') -> AKM.lookup "tag_name" version''+        version = case maybeVersion of+            Nothing -> error "Couldn't parse response from Test.WebDriverWrapper.GeckoDriver's version API"+            (Just (A.String v))-> v+            (Just _) -> error "\"tag_name\" key isn't returning a string. Maybe Test.WebDriverWrapper.GeckoDriver's version API changed, consider opening a github issue."+    return $ T.unpack version++    where+        versionAPI = geckoDriverVersionSource
+ src/Test/WebDriverWrapper/Helpers.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+Module : Test.WebDriverWrapper.Helpers+Description : Generic functions.+-}+module Test.WebDriverWrapper.Helpers (download, decompress) where++import Test.WebDriverWrapper.Constants (fileFormat, geckoDriverPath)+import Network.HTTP.Simple (setRequestHeader, setRequestMethod, httpLBS)+import Network.HTTP.Types (hUserAgent)+import Network.HTTP.Conduit (Response(..), parseRequest)+import qualified Data.ByteString.Lazy as BS+import Codec.Archive.Zip (toArchive, fromArchive)+import qualified Codec.Compression.GZip as G+import qualified Codec.Archive.Tar as Tar+import System.Posix ( setFileMode, accessModes )++-- | Downloads from @url@ at @output@ filepath. +download :: String -> FilePath -> IO()+download url output = do+    requestUrl <- parseRequest url+    let+        request+            = setRequestMethod "GET"+            $ setRequestHeader hUserAgent ["cli"]+            requestUrl+    response <- responseBody <$> httpLBS request+    BS.writeFile output response++-- | Decompresses geckodriver's download, which comes in @.zip@ for Windows or @.tar.gz@ for everyone else. +-- Takes in the archive's filepath and the output filepath. +decompress :: FilePath -> FilePath -> IO()+decompress file outputPath = do+    case fileFormat of+        ".zip"    -> do+            archive <- toArchive <$> BS.readFile file+            BS.writeFile outputPath $ fromArchive archive+        ".tar.gz" -> do+            tarball <- G.decompress <$> BS.readFile file+            Tar.unpack outputPath $ Tar.read tarball++            geckoDriver <- geckoDriverPath++            setFileMode geckoDriver accessModes+        _ -> error "unknown file"
+ src/Test/WebDriverWrapper/Selenium.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE OverloadedStrings #-}++module Test.WebDriverWrapper.Selenium (startSelenium, getSeleniumIfNeeded) where++import Test.WebDriverWrapper.Constants (defaultSeleniumJarUrl, seleniumPath, downloadPath, geckoDriverPath, seleniumLogPath)+import Test.WebDriverWrapper.Helpers (download)+import System.Directory (createDirectoryIfMissing, doesFileExist)+import Control.Monad (unless)+import GHC.IO.Handle ( Handle, hGetLine, hClose )+import System.Process (ProcessHandle, createProcess)+import Data.String.Interpolate (i)+import System.IO (openFile)+import GHC.IO.IOMode (IOMode(..))+import Control.Retry (retryOnError, RetryStatus (..))+import System.IO.Error (isEOFError)+import UnliftIO.Retry ( constantDelay, limitRetries )+import Data.Foldable.Extra (orM)+import Data.List (isInfixOf)+import System.Process.Run (proc)++-- | Starts Selenium and waits for its ok message ( "Selenium Server is up and running" ) to show up at the log file.+-- Returns the handles for the Selenium process.+startSelenium :: IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+startSelenium = do+    geckoPath <- geckoDriverPath+    selPath <- seleniumPath+    logFile <- seleniumLogPath+    writeFile logFile "" -- Create file if it doesn't exist, clears it if it does. Needs to happen for "logFileHasReadyMessage" to work correctly. +    let+        selArgs = [[i|-Dwebdriver.gecko.driver=#{geckoPath}|], "-jar", selPath ,"-log", logFile]+        processParams = proc "java" selArgs+    processHandles <- createProcess processParams+    waitForSeleniumStart+    return processHandles++waitForSeleniumStart :: IO()+waitForSeleniumStart = do+    logFile <- seleniumLogPath+    fileHandle <-  openFile logFile ReadMode+    succeeded <- retryOnError  retryPolicy'+        (\_ e -> return $ isEOFError e)+        (\retryStatus -> if rsIterNumber retryStatus < 599 then logFileHasReadyMessage fileHandle else return False)+    hClose fileHandle+    unless succeeded $ error $ "Couldn't start Selenium successfully. Check " <> logFile <> " for more information."+        where+            -- waits a minute, retrying every 100ms+            retryPolicy' = constantDelay 100000 <> limitRetries 600++logFileHasReadyMessage :: Handle -> IO Bool+logFileHasReadyMessage fileHandle = orM remainingLines+    where+        remainingLines = repeat hasReadyMessage+        hasReadyMessage = (readyMessage `isInfixOf`) <$> hGetLine fileHandle++        readyMessage = "Selenium Server is up and running"++-- | Checks if @selenium@ is in the `downloadPath`. If not, download it. +getSeleniumIfNeeded :: IO ()+getSeleniumIfNeeded = do+    selPath <- seleniumPath+    hasSelenium  <- doesFileExist selPath+    unless hasSelenium getSelenium++getSelenium :: IO ()+getSelenium = do+    downloadPath' <- downloadPath+    selPath       <- seleniumPath+    createDirectoryIfMissing True downloadPath'+    download defaultSeleniumJarUrl selPath
+ webdriver-wrapper.cabal view
@@ -0,0 +1,63 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.36.0.+--+-- see: https://github.com/sol/hpack++name:           webdriver-wrapper+version:        0.1.0.0+synopsis:       Wrapper around the webdriver package that automatically manages Selenium+description:    Please see the README on GitHub at <https://github.com/gtollini/webdriver-wrapper#readme>+category:       browser, selenium, testing, web, webdriver, geckodriver+homepage:       https://github.com/gtollini/webdriver-wrapper#readme+bug-reports:    https://github.com/gtollini/webdriver-wrapper/issues+author:         Gabriel Tollini+maintainer:     gabrieltollini@hotmail.com+copyright:      2024 Gabriel Tollini+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-doc-files:+    README.md+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/gtollini/webdriver-wrapper++library+  exposed-modules:+      Test.WebDriverWrapper+      Test.WebDriverWrapper.Constants+      Test.WebDriverWrapper.GeckoDriver+      Test.WebDriverWrapper.Helpers+      Test.WebDriverWrapper.Selenium+  other-modules:+      Paths_webdriver_wrapper+  autogen-modules:+      Paths_webdriver_wrapper+  hs-source-dirs:+      src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      aeson >=2.1.2.1 && <2.3+    , async >=2.2.5 && <2.3+    , base >=4.7 && <5+    , bytestring >=0.11.5 && <0.12+    , directory >=1.3.8 && <1.4+    , extra >=1.7.16 && <1.8+    , filepath >=1.4.300 && <1.5+    , http-conduit >=2.3.8 && <2.4+    , http-types >=0.12.4 && <0.13+    , process >=1.6.19 && <1.7+    , process-extras >=0.7.4 && <0.8+    , retry >=0.9.3 && <0.10+    , string-interpolate >=0.3.3 && <0.4+    , tar >=0.5.1.1 && <0.7+    , text >=2.0.2 && <2.1+    , unix >=2.8.4 && <2.9+    , unordered-containers >=0.2.20 && <0.3+    , webdriver >=0.12.0 && <0.13+    , zip-archive >=0.4.3 && <0.5+    , zlib >=0.6.3 && <0.8+  default-language: Haskell2010