packages feed

sandwich-webdriver 0.2.3.1 → 0.3.0.0

raw patch · 45 files changed

+2071/−1460 lines, 45 filesdep +regex-tdfadep +sandwich-contextsdep +unliftio-coredep −lifted-basedep −regex-compatdep −safe-exceptionsdep ~sandwichPVP ok

version bump matches the API change (PVP)

Dependencies added: regex-tdfa, sandwich-contexts, unliftio-core

Dependencies removed: lifted-base, regex-compat, safe-exceptions

Dependency ranges changed: sandwich

API changes (from Hackage documentation)

- Test.Sandwich.WebDriver: allocateWebDriver' :: FilePath -> WdOptions -> IO WebDriver
- Test.Sandwich.WebDriver: cleanupWebDriver' :: WebDriver -> IO ()
- Test.Sandwich.WebDriver: introduceWebDriverOptions :: forall a context m. (BaseMonadContext m context, HasCommandLineOptions context a) => WdOptions -> SpecFree (LabelValue "webdriver" WebDriver :> context) m () -> SpecFree context m ()
- Test.Sandwich.WebDriver.Class: XvfbSession :: Int -> FilePath -> (Int, Int) -> ProcessHandle -> Maybe ProcessHandle -> XvfbSession
- Test.Sandwich.WebDriver.Class: [xvfbDimensions] :: XvfbSession -> (Int, Int)
- Test.Sandwich.WebDriver.Class: [xvfbDisplayNum] :: XvfbSession -> Int
- Test.Sandwich.WebDriver.Class: [xvfbFluxboxProcess] :: XvfbSession -> Maybe ProcessHandle
- Test.Sandwich.WebDriver.Class: [xvfbProcess] :: XvfbSession -> ProcessHandle
- Test.Sandwich.WebDriver.Class: [xvfbXauthority] :: XvfbSession -> FilePath
- Test.Sandwich.WebDriver.Class: data WebDriver
- Test.Sandwich.WebDriver.Class: data XvfbSession
- Test.Sandwich.WebDriver.Class: getDisplayNumber :: WebDriver -> Maybe Int
- Test.Sandwich.WebDriver.Class: getWdOptions :: WebDriver -> WdOptions
- Test.Sandwich.WebDriver.Class: getWebDriverName :: WebDriver -> String
- Test.Sandwich.WebDriver.Class: getXvfbSession :: WebDriver -> Maybe XvfbSession
- Test.Sandwich.WebDriver.Config: DownloadChromeDriverAutodetect :: Maybe FilePath -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadChromeDriverFrom :: String -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadChromeDriverVersion :: ChromeDriverVersion -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadGeckoDriverAutodetect :: Maybe FilePath -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadGeckoDriverFrom :: String -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadGeckoDriverVersion :: GeckoDriverVersion -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: DownloadSeleniumDefault :: SeleniumToUse
- Test.Sandwich.WebDriver.Config: DownloadSeleniumFrom :: String -> SeleniumToUse
- Test.Sandwich.WebDriver.Config: UseChromeDriverAt :: FilePath -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: UseGeckoDriverAt :: FilePath -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: UseSeleniumAt :: FilePath -> SeleniumToUse
- Test.Sandwich.WebDriver.Config: chromeBinaryPath :: WdOptions -> Maybe FilePath
- Test.Sandwich.WebDriver.Config: chromeDriverToUse :: WdOptions -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: data ChromeDriverToUse
- Test.Sandwich.WebDriver.Config: data GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: data SeleniumToUse
- Test.Sandwich.WebDriver.Config: firefoxBinaryPath :: WdOptions -> Maybe FilePath
- Test.Sandwich.WebDriver.Config: geckoDriverToUse :: WdOptions -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Config: obtainChromeDriver :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadMask m) => FilePath -> ChromeDriverToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Config: obtainGeckoDriver :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> GeckoDriverToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Config: obtainSelenium :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> SeleniumToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Config: seleniumToUse :: WdOptions -> SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Action: closeAllSessions :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => WebDriver -> m ()
- Test.Sandwich.WebDriver.Internal.Action: closeAllSessionsExcept :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => [Session] -> WebDriver -> m ()
- Test.Sandwich.WebDriver.Internal.Action: closeCurrentSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadReader context m, HasLabel context "webdriver" WebDriver, HasLabel context "webdriverSession" WebDriverSession) => m ()
- Test.Sandwich.WebDriver.Internal.Action: closeSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => Session -> WebDriver -> m ()
- Test.Sandwich.WebDriver.Internal.Binaries: downloadChromeDriverIfNecessary :: Constraints m => Maybe FilePath -> FilePath -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Internal.Binaries: downloadSeleniumIfNecessary :: Constraints m => FilePath -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Internal.Binaries: obtainChromeDriver :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadMask m) => FilePath -> ChromeDriverToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Internal.Binaries: obtainGeckoDriver :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> GeckoDriverToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Internal.Binaries: obtainSelenium :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> SeleniumToUse -> m (Either Text FilePath)
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: detectChromeVersion :: Maybe FilePath -> IO (Either Text ChromeVersion)
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: getChromeDriverDownloadUrl :: ChromeDriverVersion -> Platform -> Text
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: getChromeDriverVersion :: Maybe FilePath -> IO (Either Text ChromeDriverVersion)
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.FromJSON.FromJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.JsonResponse
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.FromJSON.FromJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.PlatformAndUrl
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.FromJSON.FromJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.Version
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.ToJSON.ToJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.JsonResponse
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.ToJSON.ToJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.PlatformAndUrl
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance Data.Aeson.Types.ToJSON.ToJSON Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.Version
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Generics.Generic Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.JsonResponse
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Generics.Generic Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.PlatformAndUrl
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Generics.Generic Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.Version
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.JsonResponse
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.PlatformAndUrl
- Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome.Version
- Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox: detectFirefoxVersion :: Maybe FilePath -> IO (Either Text FirefoxVersion)
- Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox: getGeckoDriverDownloadUrl :: GeckoDriverVersion -> Platform -> Text
- Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox: getGeckoDriverVersion :: Maybe FilePath -> IO (Either Text GeckoDriverVersion)
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: Linux :: Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: OSX :: Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: Windows :: Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: data Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: detectPlatform :: Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: instance GHC.Classes.Eq Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform.Platform
- Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform.Platform
- Test.Sandwich.WebDriver.Internal.Capabilities: chromeCapabilities :: Maybe FilePath -> Capabilities
- Test.Sandwich.WebDriver.Internal.Capabilities: firefoxCapabilities :: Maybe FilePath -> Capabilities
- Test.Sandwich.WebDriver.Internal.Capabilities: getDefaultFirefoxProfile :: MonadBaseControl IO m => FilePath -> m (PreparedProfile Firefox)
- Test.Sandwich.WebDriver.Internal.Capabilities: headlessChromeCapabilities :: Maybe FilePath -> Capabilities
- Test.Sandwich.WebDriver.Internal.Capabilities: headlessFirefoxCapabilities :: Maybe FilePath -> Capabilities
- Test.Sandwich.WebDriver.Internal.Ports: findFreePortOrException :: IO PortNumber
- Test.Sandwich.WebDriver.Internal.Screenshots: saveScreenshots :: HasCallStack => Text -> WebDriver -> FilePath -> IO ()
- Test.Sandwich.WebDriver.Internal.StartWebDriver: configureDownloadCapabilities :: (MonadIO m, MonadBaseControl IO m) => [Char] -> Capabilities -> m Capabilities
- Test.Sandwich.WebDriver.Internal.StartWebDriver: configureHeadlessCapabilities :: Constraints m => WdOptions -> RunMode -> Capabilities -> m Capabilities
- Test.Sandwich.WebDriver.Internal.StartWebDriver: fromText :: Text -> Key
- Test.Sandwich.WebDriver.Internal.StartWebDriver: gracePeriod :: Int
- Test.Sandwich.WebDriver.Internal.StartWebDriver: seleniumErrFileName :: FilePath
- Test.Sandwich.WebDriver.Internal.StartWebDriver: seleniumOutFileName :: FilePath
- Test.Sandwich.WebDriver.Internal.StartWebDriver: startWebDriver :: Constraints m => WdOptions -> FilePath -> m WebDriver
- Test.Sandwich.WebDriver.Internal.StartWebDriver: startWebDriver' :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadMask m) => WdOptions -> Text -> FilePath -> [Char] -> [Char] -> [String] -> Maybe XvfbSession -> Maybe [(String, String)] -> m WebDriver
- Test.Sandwich.WebDriver.Internal.StartWebDriver: stopWebDriver :: Constraints m => WebDriver -> m ()
- Test.Sandwich.WebDriver.Internal.StartWebDriver: type Constraints m = (HasCallStack, MonadLogger m, MonadIO m, MonadBaseControl IO m, MonadMask m)
- Test.Sandwich.WebDriver.Internal.Types: Always :: WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: ChromeDriverVersionExactUrl :: (Int, Int, Int, Int) -> Text -> ChromeDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: ChromeDriverVersionTuple :: (Int, Int, Int, Int) -> ChromeDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: ChromeVersion :: (Int, Int, Int, Int) -> ChromeVersion
- Test.Sandwich.WebDriver.Internal.Types: DownloadChromeDriverAutodetect :: Maybe FilePath -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadChromeDriverFrom :: String -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadChromeDriverVersion :: ChromeDriverVersion -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadGeckoDriverAutodetect :: Maybe FilePath -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadGeckoDriverFrom :: String -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadGeckoDriverVersion :: GeckoDriverVersion -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadSeleniumDefault :: SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: DownloadSeleniumFrom :: String -> SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: FirefoxVersion :: (Int, Int, Int) -> FirefoxVersion
- Test.Sandwich.WebDriver.Internal.Types: GeckoDriverVersion :: (Int, Int, Int) -> GeckoDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: HeadlessConfig :: Maybe (Int, Int) -> HeadlessConfig
- Test.Sandwich.WebDriver.Internal.Types: InvalidLogsException :: [LogEntry] -> InvalidLogsException
- Test.Sandwich.WebDriver.Internal.Types: Never :: WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: Normal :: RunMode
- Test.Sandwich.WebDriver.Internal.Types: OnException :: WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: RunHeadless :: HeadlessConfig -> RunMode
- Test.Sandwich.WebDriver.Internal.Types: RunInXvfb :: XvfbConfig -> RunMode
- Test.Sandwich.WebDriver.Internal.Types: UseChromeDriverAt :: FilePath -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: UseGeckoDriverAt :: FilePath -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: UseSeleniumAt :: FilePath -> SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: VideoSettings :: [String] -> [String] -> [String] -> Bool -> Bool -> VideoSettings
- Test.Sandwich.WebDriver.Internal.Types: WdOptions :: ToolsRoot -> Capabilities -> WhenToSave -> SeleniumToUse -> Maybe FilePath -> ChromeDriverToUse -> Maybe FilePath -> GeckoDriverToUse -> RunMode -> Maybe Manager -> Int -> WdOptions
- Test.Sandwich.WebDriver.Internal.Types: WebDriver :: String -> (Handle, Handle, ProcessHandle, FilePath, FilePath, Maybe XvfbSession) -> WdOptions -> MVar (Map Session WDSession) -> WDConfig -> FilePath -> WebDriver
- Test.Sandwich.WebDriver.Internal.Types: XvfbConfig :: Maybe (Int, Int) -> Bool -> XvfbConfig
- Test.Sandwich.WebDriver.Internal.Types: XvfbSession :: Int -> FilePath -> (Int, Int) -> ProcessHandle -> Maybe ProcessHandle -> XvfbSession
- Test.Sandwich.WebDriver.Internal.Types: [avfoundationOptions] :: VideoSettings -> [String]
- Test.Sandwich.WebDriver.Internal.Types: [capabilities] :: WdOptions -> Capabilities
- Test.Sandwich.WebDriver.Internal.Types: [chromeBinaryPath] :: WdOptions -> Maybe FilePath
- Test.Sandwich.WebDriver.Internal.Types: [chromeDriverToUse] :: WdOptions -> ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: [firefoxBinaryPath] :: WdOptions -> Maybe FilePath
- Test.Sandwich.WebDriver.Internal.Types: [gdigrabOptions] :: VideoSettings -> [String]
- Test.Sandwich.WebDriver.Internal.Types: [geckoDriverToUse] :: WdOptions -> GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: [headlessResolution] :: HeadlessConfig -> Maybe (Int, Int)
- Test.Sandwich.WebDriver.Internal.Types: [hideMouseWhenRecording] :: VideoSettings -> Bool
- Test.Sandwich.WebDriver.Internal.Types: [httpManager] :: WdOptions -> Maybe Manager
- Test.Sandwich.WebDriver.Internal.Types: [httpRetryCount] :: WdOptions -> Int
- Test.Sandwich.WebDriver.Internal.Types: [logToDisk] :: VideoSettings -> Bool
- Test.Sandwich.WebDriver.Internal.Types: [runMode] :: WdOptions -> RunMode
- Test.Sandwich.WebDriver.Internal.Types: [saveSeleniumMessageHistory] :: WdOptions -> WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: [seleniumToUse] :: WdOptions -> SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: [toolsRoot] :: WdOptions -> ToolsRoot
- Test.Sandwich.WebDriver.Internal.Types: [wdConfig] :: WebDriver -> WDConfig
- Test.Sandwich.WebDriver.Internal.Types: [wdDownloadDir] :: WebDriver -> FilePath
- Test.Sandwich.WebDriver.Internal.Types: [wdName] :: WebDriver -> String
- Test.Sandwich.WebDriver.Internal.Types: [wdOptions] :: WebDriver -> WdOptions
- Test.Sandwich.WebDriver.Internal.Types: [wdSessionMap] :: WebDriver -> MVar (Map Session WDSession)
- Test.Sandwich.WebDriver.Internal.Types: [wdWebDriver] :: WebDriver -> (Handle, Handle, ProcessHandle, FilePath, FilePath, Maybe XvfbSession)
- Test.Sandwich.WebDriver.Internal.Types: [x11grabOptions] :: VideoSettings -> [String]
- Test.Sandwich.WebDriver.Internal.Types: [xvfbDimensions] :: XvfbSession -> (Int, Int)
- Test.Sandwich.WebDriver.Internal.Types: [xvfbDisplayNum] :: XvfbSession -> Int
- Test.Sandwich.WebDriver.Internal.Types: [xvfbFluxboxProcess] :: XvfbSession -> Maybe ProcessHandle
- Test.Sandwich.WebDriver.Internal.Types: [xvfbProcess] :: XvfbSession -> ProcessHandle
- Test.Sandwich.WebDriver.Internal.Types: [xvfbResolution] :: XvfbConfig -> Maybe (Int, Int)
- Test.Sandwich.WebDriver.Internal.Types: [xvfbStartFluxbox] :: XvfbConfig -> Bool
- Test.Sandwich.WebDriver.Internal.Types: [xvfbXauthority] :: XvfbSession -> FilePath
- Test.Sandwich.WebDriver.Internal.Types: class HasWebDriver a
- Test.Sandwich.WebDriver.Internal.Types: data ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: data ChromeDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: data GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: data HeadlessConfig
- Test.Sandwich.WebDriver.Internal.Types: data InvalidLogsException
- Test.Sandwich.WebDriver.Internal.Types: data RunMode
- Test.Sandwich.WebDriver.Internal.Types: data SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: data VideoSettings
- Test.Sandwich.WebDriver.Internal.Types: data WdOptions
- Test.Sandwich.WebDriver.Internal.Types: data WebDriver
- Test.Sandwich.WebDriver.Internal.Types: data WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: data XvfbConfig
- Test.Sandwich.WebDriver.Internal.Types: data XvfbSession
- Test.Sandwich.WebDriver.Internal.Types: defaultAvfoundationOptions :: [String]
- Test.Sandwich.WebDriver.Internal.Types: defaultGdigrabOptions :: [String]
- Test.Sandwich.WebDriver.Internal.Types: defaultHeadlessConfig :: HeadlessConfig
- Test.Sandwich.WebDriver.Internal.Types: defaultVideoSettings :: VideoSettings
- Test.Sandwich.WebDriver.Internal.Types: defaultWdOptions :: FilePath -> WdOptions
- Test.Sandwich.WebDriver.Internal.Types: defaultXvfbConfig :: XvfbConfig
- Test.Sandwich.WebDriver.Internal.Types: fastX11VideoOptions :: [String]
- Test.Sandwich.WebDriver.Internal.Types: getDisplayNumber :: WebDriver -> Maybe Int
- Test.Sandwich.WebDriver.Internal.Types: getWdOptions :: WebDriver -> WdOptions
- Test.Sandwich.WebDriver.Internal.Types: getWebDriver :: HasWebDriver a => a -> WebDriver
- Test.Sandwich.WebDriver.Internal.Types: getWebDriverName :: WebDriver -> String
- Test.Sandwich.WebDriver.Internal.Types: getXvfbSession :: WebDriver -> Maybe XvfbSession
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Classes.Eq Test.Sandwich.WebDriver.Internal.Types.WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Exception.Type.Exception Test.Sandwich.WebDriver.Internal.Types.InvalidLogsException
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.ChromeDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.ChromeDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.ChromeVersion
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.FirefoxVersion
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.GeckoDriverToUse
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.GeckoDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.InvalidLogsException
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.SeleniumToUse
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.WhenToSave
- Test.Sandwich.WebDriver.Internal.Types: instance GHC.Show.Show Test.Sandwich.WebDriver.Internal.Types.XvfbSession
- Test.Sandwich.WebDriver.Internal.Types: instance Test.Sandwich.WebDriver.Internal.Types.HasWebDriver Test.Sandwich.WebDriver.Internal.Types.WebDriver
- Test.Sandwich.WebDriver.Internal.Types: newtype ChromeVersion
- Test.Sandwich.WebDriver.Internal.Types: newtype FirefoxVersion
- Test.Sandwich.WebDriver.Internal.Types: newtype GeckoDriverVersion
- Test.Sandwich.WebDriver.Internal.Types: qualityX11VideoOptions :: [String]
- Test.Sandwich.WebDriver.Internal.Types: type Session = String
- Test.Sandwich.WebDriver.Internal.Types: type ToolsRoot = FilePath
- Test.Sandwich.WebDriver.Internal.Types: type WebDriverContext context wd = (HasLabel context "webdriver" WebDriver, WebDriver (ExampleT context wd))
- Test.Sandwich.WebDriver.Internal.Types: type WebDriverSession = (Session, IORef WDSession)
- Test.Sandwich.WebDriver.Internal.Types: webdriver :: Label "webdriver" WebDriver
- Test.Sandwich.WebDriver.Internal.Types: webdriverSession :: Label "webdriverSession" WebDriverSession
- Test.Sandwich.WebDriver.Internal.Util: gracefullyStopProcess :: (MonadIO m, MonadLogger m) => ProcessHandle -> Int -> m ()
- Test.Sandwich.WebDriver.Internal.Util: gracefullyWaitForProcess :: (MonadIO m, MonadLogger m) => ProcessHandle -> Int -> m ()
- Test.Sandwich.WebDriver.Internal.Util: leftOnException :: (MonadIO m, MonadBaseControl IO m) => m (Either Text a) -> m (Either Text a)
- Test.Sandwich.WebDriver.Internal.Util: leftOnException' :: (MonadIO m, MonadBaseControl IO m) => m a -> m (Either Text a)
- Test.Sandwich.WebDriver.Internal.Util: makeUUID :: IO Text
- Test.Sandwich.WebDriver.Internal.Util: moveAndTruncate :: FilePath -> String -> IO ()
- Test.Sandwich.WebDriver.Internal.Util: whenJust :: Monad m => Maybe a -> (a -> m b) -> m ()
- Test.Sandwich.WebDriver.Internal.Util: whenLeft :: Monad m => Either a b -> (a -> m ()) -> m ()
- Test.Sandwich.WebDriver.Internal.Util: whenRight :: Monad m => Either a b -> (b -> m ()) -> m ()
- Test.Sandwich.WebDriver.Internal.Video: getVideoArgs :: (MonadIO m, Interpolatable 'False p String, Interpolatable 'False a String, Interpolatable 'False b String, Interpolatable 'False c String, Interpolatable 'False d String) => p -> (a, b, c, d) -> VideoSettings -> Maybe XvfbSession -> m CreateProcess
- Test.Sandwich.WebDriver.Types: hoistExample :: ExampleT context IO a -> ExampleT (ContextWithSession context) IO a
- Test.Sandwich.WebDriver.Types: instance (Control.Monad.IO.Class.MonadIO m, Control.Monad.Catch.MonadThrow m, Test.Sandwich.Types.Spec.HasLabel context "webdriverSession" Test.Sandwich.WebDriver.Internal.Types.WebDriverSession, Control.Monad.Trans.Control.MonadBaseControl GHC.Types.IO m) => Test.WebDriver.Class.WebDriver (Test.Sandwich.Types.Spec.ExampleT context m)
- Test.Sandwich.WebDriver.Types: instance (Control.Monad.IO.Class.MonadIO m, Test.Sandwich.Types.Spec.HasLabel context "webdriverSession" Test.Sandwich.WebDriver.Internal.Types.WebDriverSession) => Test.WebDriver.Session.WDSessionState (Test.Sandwich.Types.Spec.ExampleT context m)
- Test.Sandwich.WebDriver.Types: type BaseMonad m = (HasCallStack, MonadIO m, MonadCatch m, MonadBaseControl IO m, MonadMask m)
- Test.Sandwich.WebDriver.Types: type BaseMonadContext m context = (BaseMonad m, HasBaseContext context)
- Test.Sandwich.WebDriver.Types: type ContextWithSession context = LabelValue "webdriverSession" WebDriverSession :> context
- Test.Sandwich.WebDriver.Types: type ExampleWithWebDriver context wd = (WDSessionState (ExampleT context wd), WebDriver wd)
- Test.Sandwich.WebDriver.Types: type HasWebDriverContext context = HasLabel context "webdriver" WebDriver
- Test.Sandwich.WebDriver.Types: type HasWebDriverSessionContext context = HasLabel context "webdriverSession" WebDriverSession
- Test.Sandwich.WebDriver.Types: type WebDriverMonad m context = (HasCallStack, HasLabel context "webdriver" WebDriver, MonadIO m, MonadBaseControl IO m)
- Test.Sandwich.WebDriver.Types: type WebDriverSessionMonad m context = (WebDriverMonad m context, MonadReader context m, HasLabel context "webdriverSession" WebDriverSession)
- Test.Sandwich.WebDriver.Types: wdDownloadDir :: WebDriver -> FilePath
- Test.Sandwich.WebDriver.Types: webdriver :: Label "webdriver" WebDriver
- Test.Sandwich.WebDriver.Video: [x11grabOptions] :: VideoSettings -> [String]
+ Test.Sandwich.WebDriver: WebDriverDependencies :: Maybe FilePath -> SeleniumToUse -> BrowserDependenciesSpec -> XvfbDependenciesSpec -> FfmpegToUse -> WebDriverDependencies
+ Test.Sandwich.WebDriver: [webDriverDependencyBrowser] :: WebDriverDependencies -> BrowserDependenciesSpec
+ Test.Sandwich.WebDriver: [webDriverDependencyJava] :: WebDriverDependencies -> Maybe FilePath
+ Test.Sandwich.WebDriver: [webDriverDependencySelenium] :: WebDriverDependencies -> SeleniumToUse
+ Test.Sandwich.WebDriver: [webDriverFfmpeg] :: WebDriverDependencies -> FfmpegToUse
+ Test.Sandwich.WebDriver: [webDriverXvfb] :: WebDriverDependencies -> XvfbDependenciesSpec
+ Test.Sandwich.WebDriver: data OnDemandOptions
+ Test.Sandwich.WebDriver: data WebDriver
+ Test.Sandwich.WebDriver: data WebDriverDependencies
+ Test.Sandwich.WebDriver: defaultOnDemandOptions :: OnDemandOptions
+ Test.Sandwich.WebDriver: defaultWebDriverDependencies :: WebDriverDependencies
+ Test.Sandwich.WebDriver: introduceBrowserDependenciesViaNix :: forall m context. (MonadUnliftIO m, HasBaseContext context, HasNixContext context, HasSomeCommandLineOptions context) => SpecFree (LabelValue "browserDependencies" BrowserDependencies :> context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: introduceBrowserDependenciesViaNix' :: forall m context. (MonadUnliftIO m, HasBaseContext context, HasNixContext context, HasSomeCommandLineOptions context) => NodeOptions -> SpecFree (LabelValue "browserDependencies" BrowserDependencies :> context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: introduceWebDriver' :: forall m context. BaseMonad m context => WebDriverDependencies -> (WdOptions -> ExampleT (ContextWithBaseDeps context) m WebDriver) -> WdOptions -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: introduceWebDriverViaNix :: forall m context. (BaseMonad m context, HasSomeCommandLineOptions context, HasNixContext context) => WdOptions -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: introduceWebDriverViaNix' :: forall m context. (BaseMonad m context, HasSomeCommandLineOptions context, HasNixContext context) => NodeOptions -> WdOptions -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: type BaseMonad m context = (HasCallStack, MonadUnliftIO m, MonadMask m, HasBaseContext context)
+ Test.Sandwich.WebDriver: type ContextWithBaseDeps context = -- | Browser dependencies LabelValue "browserDependencies" BrowserDependencies :> FileValue "java" :> FileValue "selenium.jar" :> context
+ Test.Sandwich.WebDriver: type ContextWithWebdriverDeps context = LabelValue "webdriver" WebDriver :> ContextWithBaseDeps context
+ Test.Sandwich.WebDriver: type HasWebDriverContext context = HasLabel context "webdriver" WebDriver
+ Test.Sandwich.WebDriver: type HasWebDriverSessionContext context = HasLabel context "webdriverSession" WebDriverSession
+ Test.Sandwich.WebDriver: type WebDriverMonad m context = (HasCallStack, MonadUnliftIO m, HasWebDriverContext context)
+ Test.Sandwich.WebDriver: type WebDriverSession = (Session, IORef WDSession)
+ Test.Sandwich.WebDriver: type WebDriverSessionMonad m context = (WebDriverMonad m context, MonadReader context m, HasWebDriverSessionContext context)
+ Test.Sandwich.WebDriver: webdriver :: Label "webdriver" WebDriver
+ Test.Sandwich.WebDriver: webdriverSession :: Label "webdriverSession" WebDriverSession
+ Test.Sandwich.WebDriver.Binaries: ChromeDriverVersionExactUrl :: (Int, Int, Int, Int) -> Text -> ChromeDriverVersion
+ Test.Sandwich.WebDriver.Binaries: ChromeDriverVersionTuple :: (Int, Int, Int, Int) -> ChromeDriverVersion
+ Test.Sandwich.WebDriver.Binaries: ChromeVersion :: (Int, Int, Int, Int) -> ChromeVersion
+ Test.Sandwich.WebDriver.Binaries: DownloadChromeDriverAutodetect :: FilePath -> FilePath -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadChromeDriverFrom :: FilePath -> String -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadChromeDriverVersion :: FilePath -> ChromeDriverVersion -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadGeckoDriverAutodetect :: FilePath -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadGeckoDriverFrom :: FilePath -> String -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadGeckoDriverVersion :: FilePath -> GeckoDriverVersion -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadSeleniumDefault :: FilePath -> SeleniumToUse
+ Test.Sandwich.WebDriver.Binaries: DownloadSeleniumFrom :: FilePath -> String -> SeleniumToUse
+ Test.Sandwich.WebDriver.Binaries: GeckoDriverVersion :: (Int, Int, Int) -> GeckoDriverVersion
+ Test.Sandwich.WebDriver.Binaries: UseChromeAt :: FilePath -> ChromeToUse
+ Test.Sandwich.WebDriver.Binaries: UseChromeDriverAt :: FilePath -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: UseChromeDriverFromNixpkgs :: NixContext -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: UseChromeFromNixpkgs :: NixContext -> ChromeToUse
+ Test.Sandwich.WebDriver.Binaries: UseChromeFromPath :: ChromeToUse
+ Test.Sandwich.WebDriver.Binaries: UseFfmpegAt :: FilePath -> FfmpegToUse
+ Test.Sandwich.WebDriver.Binaries: UseFfmpegFromNixpkgs :: NixContext -> FfmpegToUse
+ Test.Sandwich.WebDriver.Binaries: UseFfmpegFromPath :: FfmpegToUse
+ Test.Sandwich.WebDriver.Binaries: UseFirefoxAt :: FilePath -> FirefoxToUse
+ Test.Sandwich.WebDriver.Binaries: UseFirefoxFromNixpkgs :: NixContext -> FirefoxToUse
+ Test.Sandwich.WebDriver.Binaries: UseFirefoxFromPath :: FirefoxToUse
+ Test.Sandwich.WebDriver.Binaries: UseFluxboxAt :: FilePath -> FluxboxToUse
+ Test.Sandwich.WebDriver.Binaries: UseFluxboxFromNixpkgs :: NixContext -> FluxboxToUse
+ Test.Sandwich.WebDriver.Binaries: UseFluxboxFromPath :: FluxboxToUse
+ Test.Sandwich.WebDriver.Binaries: UseGeckoDriverAt :: FilePath -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: UseGeckoDriverFromNixpkgs :: NixContext -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: UseSeleniumAt :: FilePath -> SeleniumToUse
+ Test.Sandwich.WebDriver.Binaries: UseSeleniumFromNixpkgs :: NixContext -> SeleniumToUse
+ Test.Sandwich.WebDriver.Binaries: UseXvfbAt :: FilePath -> XvfbToUse
+ Test.Sandwich.WebDriver.Binaries: UseXvfbFromNixpkgs :: NixContext -> XvfbToUse
+ Test.Sandwich.WebDriver.Binaries: UseXvfbFromPath :: XvfbToUse
+ Test.Sandwich.WebDriver.Binaries: XvfbDependenciesSpec :: XvfbToUse -> Maybe FluxboxToUse -> XvfbDependenciesSpec
+ Test.Sandwich.WebDriver.Binaries: [xvfbDependenciesSpecFluxbox] :: XvfbDependenciesSpec -> Maybe FluxboxToUse
+ Test.Sandwich.WebDriver.Binaries: [xvfbDependenciesSpecXvfb] :: XvfbDependenciesSpec -> XvfbToUse
+ Test.Sandwich.WebDriver.Binaries: data ChromeDriverToUse
+ Test.Sandwich.WebDriver.Binaries: data ChromeDriverVersion
+ Test.Sandwich.WebDriver.Binaries: data ChromeToUse
+ Test.Sandwich.WebDriver.Binaries: data FfmpegToUse
+ Test.Sandwich.WebDriver.Binaries: data FirefoxToUse
+ Test.Sandwich.WebDriver.Binaries: data FluxboxToUse
+ Test.Sandwich.WebDriver.Binaries: data GeckoDriverToUse
+ Test.Sandwich.WebDriver.Binaries: data SeleniumToUse
+ Test.Sandwich.WebDriver.Binaries: data XvfbDependenciesSpec
+ Test.Sandwich.WebDriver.Binaries: data XvfbToUse
+ Test.Sandwich.WebDriver.Binaries: newtype ChromeVersion
+ Test.Sandwich.WebDriver.Binaries: newtype GeckoDriverVersion
+ Test.Sandwich.WebDriver.Binaries: obtainChrome :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLogger m) => ChromeToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Binaries: obtainChromeDriver :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLogger m) => ChromeDriverToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Binaries: obtainFfmpeg :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLoggerIO m, MonadMask m) => FfmpegToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Binaries: obtainFirefox :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLogger m) => FirefoxToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Binaries: obtainGeckoDriver :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLogger m) => GeckoDriverToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Binaries: obtainSelenium :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLogger m) => SeleniumToUse -> m FilePath
+ Test.Sandwich.WebDriver.Binaries: obtainXvfb :: (MonadReader context m, HasBaseContext context, MonadUnliftIO m, MonadLoggerIO m) => XvfbToUse -> m (Either Text FilePath)
+ Test.Sandwich.WebDriver.Config: BrowserDependenciesChrome :: FilePath -> FilePath -> BrowserDependencies
+ Test.Sandwich.WebDriver.Config: BrowserDependenciesFirefox :: FilePath -> FilePath -> BrowserDependencies
+ Test.Sandwich.WebDriver.Config: BrowserDependenciesSpecChrome :: ChromeToUse -> ChromeDriverToUse -> BrowserDependenciesSpec
+ Test.Sandwich.WebDriver.Config: BrowserDependenciesSpecFirefox :: FirefoxToUse -> GeckoDriverToUse -> BrowserDependenciesSpec
+ Test.Sandwich.WebDriver.Config: XvfbSession :: Int -> FilePath -> (Int, Int) -> ProcessHandle -> Maybe ProcessHandle -> XvfbSession
+ Test.Sandwich.WebDriver.Config: [browserDependenciesChromeChrome] :: BrowserDependencies -> FilePath
+ Test.Sandwich.WebDriver.Config: [browserDependenciesChromeChromedriver] :: BrowserDependencies -> FilePath
+ Test.Sandwich.WebDriver.Config: [browserDependenciesFirefoxFirefox] :: BrowserDependencies -> FilePath
+ Test.Sandwich.WebDriver.Config: [browserDependenciesFirefoxGeckodriver] :: BrowserDependencies -> FilePath
+ Test.Sandwich.WebDriver.Config: [browserDependenciesSpecChromeChromeDriver] :: BrowserDependenciesSpec -> ChromeDriverToUse
+ Test.Sandwich.WebDriver.Config: [browserDependenciesSpecChromeChrome] :: BrowserDependenciesSpec -> ChromeToUse
+ Test.Sandwich.WebDriver.Config: [browserDependenciesSpecFirefoxFirefox] :: BrowserDependenciesSpec -> FirefoxToUse
+ Test.Sandwich.WebDriver.Config: [browserDependenciesSpecFirefoxGeckodriver] :: BrowserDependenciesSpec -> GeckoDriverToUse
+ Test.Sandwich.WebDriver.Config: [xvfbDimensions] :: XvfbSession -> (Int, Int)
+ Test.Sandwich.WebDriver.Config: [xvfbDisplayNum] :: XvfbSession -> Int
+ Test.Sandwich.WebDriver.Config: [xvfbFluxboxProcess] :: XvfbSession -> Maybe ProcessHandle
+ Test.Sandwich.WebDriver.Config: [xvfbProcess] :: XvfbSession -> ProcessHandle
+ Test.Sandwich.WebDriver.Config: [xvfbXauthority] :: XvfbSession -> FilePath
+ Test.Sandwich.WebDriver.Config: browserDependencies :: Label "browserDependencies" BrowserDependencies
+ Test.Sandwich.WebDriver.Config: data BrowserDependencies
+ Test.Sandwich.WebDriver.Config: data BrowserDependenciesSpec
+ Test.Sandwich.WebDriver.Config: data XvfbSession
+ Test.Sandwich.WebDriver.Config: getDisplayNumber :: WebDriver -> Maybe Int
+ Test.Sandwich.WebDriver.Config: getDownloadDirectory :: WebDriver -> FilePath
+ Test.Sandwich.WebDriver.Config: getWdOptions :: WebDriver -> WdOptions
+ Test.Sandwich.WebDriver.Config: getWebDriverName :: WebDriver -> String
+ Test.Sandwich.WebDriver.Config: getXvfbSession :: WebDriver -> Maybe XvfbSession
+ Test.Sandwich.WebDriver.Config: type HasBrowserDependencies context = HasLabel context "browserDependencies" BrowserDependencies
+ Test.Sandwich.WebDriver.Video: [xcbgrabOptions] :: VideoSettings -> [String]
+ Test.Sandwich.WebDriver.Video: data VideoProcess
+ Test.Sandwich.WebDriver.Video: recordVideoIfConfigured :: (BaseVideoConstraints context m, WebDriver m, HasSomeCommandLineOptions context) => String -> m a -> m a
+ Test.Sandwich.WebDriver.Video: type BaseVideoConstraints context m = (MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadReader context m, HasBaseContext context, HasWebDriverContext context)
+ Test.Sandwich.WebDriver.Video: videoProcessProcess :: VideoProcess -> ProcessHandle
+ Test.Sandwich.WebDriver.Windows: getResolution :: HasCallStack => IO (Int, Int, Int, Int)
+ Test.Sandwich.WebDriver.Windows: getResolutionForDisplay :: HasCallStack => Int -> IO (Int, Int, Int, Int)
- Test.Sandwich.WebDriver: addCommandLineOptionsToWdOptions :: CommandLineOptions a -> WdOptions -> WdOptions
+ Test.Sandwich.WebDriver: addCommandLineOptionsToWdOptions :: SomeCommandLineOptions -> WdOptions -> WdOptions
- Test.Sandwich.WebDriver: allocateWebDriver :: (HasBaseContext context, BaseMonad m) => WdOptions -> ExampleT context m WebDriver
+ Test.Sandwich.WebDriver: allocateWebDriver :: (BaseMonad m context, HasFile context "java", HasFile context "selenium.jar", HasBrowserDependencies context) => WdOptions -> OnDemandOptions -> ExampleT context m WebDriver
- Test.Sandwich.WebDriver: cleanupWebDriver :: (HasBaseContext context, BaseMonad m) => WebDriver -> ExampleT context m ()
+ Test.Sandwich.WebDriver: cleanupWebDriver :: BaseMonad m context => WebDriver -> ExampleT context m ()
- Test.Sandwich.WebDriver: closeAllSessions :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => WebDriver -> m ()
+ Test.Sandwich.WebDriver: closeAllSessions :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => WebDriver -> m ()
- Test.Sandwich.WebDriver: closeAllSessionsExcept :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => [Session] -> WebDriver -> m ()
+ Test.Sandwich.WebDriver: closeAllSessionsExcept :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => [Session] -> WebDriver -> m ()
- Test.Sandwich.WebDriver: closeCurrentSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadReader context m, HasLabel context "webdriver" WebDriver, HasLabel context "webdriverSession" WebDriverSession) => m ()
+ Test.Sandwich.WebDriver: closeCurrentSession :: (MonadLogger m, WebDriverSessionMonad m context) => m ()
- Test.Sandwich.WebDriver: closeSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => Session -> WebDriver -> m ()
+ Test.Sandwich.WebDriver: closeSession :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => Session -> WebDriver -> m ()
- Test.Sandwich.WebDriver: getSessions :: (WebDriverMonad m context, MonadReader context m, HasLabel context "webdriver" WebDriver) => m [Session]
+ Test.Sandwich.WebDriver: getSessions :: (MonadReader context m, WebDriverMonad m context) => m [Session]
- Test.Sandwich.WebDriver: introduceWebDriver :: BaseMonadContext m context => WdOptions -> SpecFree (LabelValue "webdriver" WebDriver :> context) m () -> SpecFree context m ()
+ Test.Sandwich.WebDriver: introduceWebDriver :: forall context m. (BaseMonad m context, HasSomeCommandLineOptions context) => WebDriverDependencies -> WdOptions -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()
- Test.Sandwich.WebDriver: withSession :: forall m context a. WebDriverMonad m context => Session -> ExampleT (ContextWithSession context) m a -> ExampleT context m a
+ Test.Sandwich.WebDriver: withSession :: forall m context a. (MonadMask m, MonadBaseControl IO m, HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context) => Session -> ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a -> ExampleT context m a
- Test.Sandwich.WebDriver: withSession1 :: WebDriverMonad m context => ExampleT (ContextWithSession context) m a -> ExampleT context m a
+ Test.Sandwich.WebDriver: withSession1 :: (MonadMask m, MonadBaseControl IO m, HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context) => ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a -> ExampleT context m a
- Test.Sandwich.WebDriver: withSession2 :: WebDriverMonad m context => ExampleT (ContextWithSession context) m a -> ExampleT context m a
+ Test.Sandwich.WebDriver: withSession2 :: (MonadMask m, MonadBaseControl IO m, HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context) => ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a -> ExampleT context m a
- Test.Sandwich.WebDriver.Config: defaultWdOptions :: FilePath -> WdOptions
+ Test.Sandwich.WebDriver.Config: defaultWdOptions :: WdOptions
- Test.Sandwich.WebDriver.Video: endVideoRecording :: (MonadLoggerIO m, MonadCatch m) => ProcessHandle -> m ()
+ Test.Sandwich.WebDriver.Video: endVideoRecording :: (MonadLoggerIO m, MonadUnliftIO m) => VideoProcess -> m ()
- Test.Sandwich.WebDriver.Video: startBrowserVideoRecording :: (BaseVideoConstraints context m, MonadThrow m, HasWebDriverSessionContext context, WebDriver m) => FilePath -> VideoSettings -> m ProcessHandle
+ Test.Sandwich.WebDriver.Video: startBrowserVideoRecording :: (BaseVideoConstraints context m, WebDriver m) => FilePath -> VideoSettings -> m VideoProcess
- Test.Sandwich.WebDriver.Video: startFullScreenVideoRecording :: (BaseVideoConstraints context m, MonadMask m) => FilePath -> VideoSettings -> m ProcessHandle
+ Test.Sandwich.WebDriver.Video: startFullScreenVideoRecording :: BaseVideoConstraints context m => FilePath -> VideoSettings -> m VideoProcess
- Test.Sandwich.WebDriver.Video: startVideoRecording :: BaseVideoConstraints context m => FilePath -> (Word, Word, Int, Int) -> VideoSettings -> m ProcessHandle
+ Test.Sandwich.WebDriver.Video: startVideoRecording :: BaseVideoConstraints context m => FilePath -> (Word, Word, Int, Int) -> VideoSettings -> m VideoProcess
- Test.Sandwich.WebDriver.Windows: getScreenResolution :: (HasCallStack, MonadIO m, MonadMask m, MonadLogger m) => WebDriver -> m (Int, Int, Int, Int)
+ Test.Sandwich.WebDriver.Windows: getScreenResolution :: MonadIO m => WebDriver -> m (Int, Int, Int, Int)
- Test.Sandwich.WebDriver.Windows: setWindowFullScreen :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()
+ Test.Sandwich.WebDriver.Windows: setWindowFullScreen :: (WebDriverMonad m context, MonadReader context m, WebDriver m) => m ()
- Test.Sandwich.WebDriver.Windows: setWindowLeftSide :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()
+ Test.Sandwich.WebDriver.Windows: setWindowLeftSide :: (WebDriverMonad m context, MonadReader context m, WebDriver m) => m ()
- Test.Sandwich.WebDriver.Windows: setWindowRightSide :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()
+ Test.Sandwich.WebDriver.Windows: setWindowRightSide :: (WebDriverMonad m context, MonadReader context m, WebDriver m) => m ()

Files

CHANGELOG.md view
@@ -2,6 +2,19 @@  # Unreleased +# 0.3.0.0++* BREAKING CHANGE: switch most monads away from using `MonadBaseControl IO` and switch to `MonadUnliftIO`. We also remove `MonadThrow` constraints, relying only on `MonadIO` for throwing exceptions.+* Fix window positioning commands to use window.devicePixelRatio.+* Add support for introducing Selenium dependencies using Nix with `sandwich-contexts`.+* Improve Haddocks and simplify module structure.+* Export `getDownloadDirectory` accessor for `WebDriver`.+* Be able to obtain dependencies like `ffmpeg` and `Xvfb` on demand.+* Clean up dependencies and fix some warnings on MacOS and Windows.+* Be able to pass a custom Firefox profile in `Capabilities`.+* Remove `hoistExample` helper which didn't belong in this package.+* Support video recording flags `--error-videos`/`--individual-videos`.+ # 0.2.3.1  * Binary fetching: don't create the toolsRoot directory unless necessary.
LICENSE view
@@ -1,4 +1,4 @@-Copyright Tom McLaughlin (c) 2023+Copyright Tom McLaughlin (c) 2024  All rights reserved. 
− app/Main.hs
@@ -1,100 +0,0 @@-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE CPP #-}--module Main where--import Control.Concurrent-import Control.Monad.IO.Class-import Data.Maybe-import Data.String.Interpolate-import Data.Time.Clock-import Test.Sandwich-import Test.Sandwich.Formatters.Print-import Test.Sandwich.WebDriver-import Test.Sandwich.WebDriver.Windows-import Test.WebDriver--#ifndef mingw32_HOST_OS-import Test.Sandwich.Formatters.TerminalUI-#endif---simple :: TopSpec-simple = introduceWebDriver wdOptions $ do-  it "does the thing 1" $ withSession1 $ do-    openPage "http://www.google.com"-    setWindowLeftSide-    search <- findElem (ByCSS [i|*[title="Search"]|])-    click search-    sendKeys "asdf" search-    liftIO $ threadDelay 1000000-    sendKeys "fdsa" search-    liftIO $ threadDelay 1000000-    sendKeys "jkl" search-    liftIO $ threadDelay 1000000-    findElem (ByCSS ".does-not-exist")-    expectationFailure "OH NO"-  -- it "does the thing 2" $ withSession2 $ do-  --   openPage "http://www.cnn.com"-  --   setWindowRightSide-  --   liftIO $ threadDelay 1000000---- concurrent :: TopSpec--- concurrent = introduceWebDriver wdOptions $ parallel $ do---   it "does the thing 1" $ withSession1 $ do---     openPage "http://www.google.com"---     setWindowLeftSide---     liftIO $ threadDelay 10000000---   it "does the thing 2" $ withSession2 $ do---     openPage "http://www.cnn.com"---     setWindowRightSide---     liftIO $ threadDelay 10000000---- pooled :: TopSpec--- pooled = do---   introduce "WebDriver pool" webdriverPool doCreatePool (liftIO . purgePool) $ parallel $ do---     it "works" (2 `shouldBe` 2)----     claimWebDriver $ do---       it "does the thing 1" $ withSession1 $ do---         openPage "http://www.google.com"---         setWindowLeftSide---         liftIO $ threadDelay 1000000----     claimWebDriver $ do---       it "does the thing 2" $ withSession1 $ do---         openPage "http://www.cnn.com"---         setWindowRightSide---         liftIO $ threadDelay 1000000---- doCreatePool = do---   maybeRunRoot <- getRunRoot---   let runRoot = fromMaybe "/tmp" maybeRunRoot---   liftIO $ createPool (allocateWebDriver' runRoot wdOptions) cleanupWebDriver' 1 (5) 4---- claimWebDriver subspec = introduceWith "Claim webdriver" webdriver wrappedAction subspec---   where wrappedAction action = do---           pool <- getContext webdriverPool---           debug "Trying to claim webdriver"---           withResource pool (liftIO . action)---- webdriverPool = Label :: Label "webdriverPool" (Pool WdSession)--wdOptions = (defaultWdOptions "/tmp/tools") {-  -- capabilities = chromeCapabilities-  capabilities = firefoxCapabilities Nothing-  -- capabilities = headlessFirefoxCapabilities-  , saveSeleniumMessageHistory = Always-  -- , runMode = Normal-  , runMode = RunHeadless defaultHeadlessConfig-  }--testOptions = defaultOptions {-  optionsTestArtifactsDirectory = TestArtifactsGeneratedDirectory "test_runs" (show <$> getCurrentTime)-  -- , optionsFormatters = [SomeFormatter defaultTerminalUIFormatter]-  , optionsFormatters = [SomeFormatter defaultPrintFormatter]-  }--main :: IO ()-main = runSandwich testOptions simple
− app/Simple.hs
@@ -1,22 +0,0 @@--module Simple where--import Test.Sandwich-import Test.Sandwich.WebDriver-import Test.WebDriver--wdOptions = (defaultWdOptions "/tmp/tools") {-  capabilities = firefoxCapabilities Nothing-  , runMode = RunHeadless defaultHeadlessConfig-  }--spec :: TopSpec-spec = introduceWebDriver wdOptions $ do-  it "opens Google and searches" $ withSession1 $ do-    openPage "http://www.google.com"-    search <- findElem (ByCSS "*[title='Search']")-    click search-    sendKeys "asdf\n" search--main :: IO ()-main = runSandwich defaultOptions spec
linux-src/Test/Sandwich/WebDriver/Resolution.hs view
@@ -1,4 +1,10 @@+{-# OPTIONS_GHC -fno-warn-redundant-constraints #-} +{-|+Helper module to obtain the current display resolution. This is useful for positioning windows or setting up video recording.+-}++ module Test.Sandwich.WebDriver.Resolution (   getResolution   , getResolutionForDisplay@@ -9,32 +15,47 @@ import qualified Data.List as L import Data.String.Interpolate import qualified Data.Text as T+import GHC.Stack import Safe import System.Directory import System.Exit import System.Process-import Text.Regex+import Text.Regex.TDFA  --- | Previously we got the screen resolution on Linux using the X11 Haskell library.+-- | Note: previously we got the screen resolution on Linux using the X11 Haskell library.+-- -- This was a troublesome dependency because it wouldn't build on Hackage, forcing us to upload--- sandwich-webdriver documentation manually.+-- the documentation manually.+-- -- It also caused problems when trying to make the demos easy to run on a clean machine or a Mac.--- Instead, we implement platform-specific getResolution functions.--- On Linux, the simplest way seems to be to parse the output of xrandr. This is the approach taken by--- at least one other library: https://github.com/davidmarkclements/screenres/blob/master/linux.cc--- The other way to do it would be to load the x11 and/or xinerama libraries like is done here:--- https://github.com/rr-/screeninfo/blob/master/screeninfo/enumerators/xinerama.py--- but again, that would require users to install those libraries. xrandr itself seems like an easier+-- So instead, we now implement platform-specific 'getResolution' functions.+--+-- On Linux, the simplest way seems to be to parse the output of @xrandr@. This is the approach taken by+-- at least one other library called [screenres](https://github.com/davidmarkclements/screenres/blob/master/linux.cc).+-- The other way to do it would be to load the x11 and/or xinerama libraries like is done in+-- [screeninfo](https://github.com/rr-/screeninfo/blob/master/screeninfo/enumerators/xinerama.py),+-- but again, that would require users to install those libraries. Just using @xrandr@ itself seems like an easier -- dependency.-getResolution :: IO (Int, Int, Int, Int)+getResolution :: (+  HasCallStack+  )+  -- | Returns (x, y, width, height)+  => IO (Int, Int, Int, Int) getResolution = getResolution' Nothing -getResolutionForDisplay :: Int -> IO (Int, Int, Int, Int)+-- | Get the resolution for a specific display.+getResolutionForDisplay :: (+  HasCallStack+  )+  -- | Display number+  => Int+  -- | Returns (x, y, width, height)+  -> IO (Int, Int, Int, Int) getResolutionForDisplay n = getResolution' (Just [("DISPLAY", ":" <> show n)])  -- | Note: this doesn't pick up display scaling on Ubuntu 20.04.-getResolution' :: Maybe [(String, String)] -> IO (Int, Int, Int, Int)+getResolution' :: (HasCallStack) => Maybe [(String, String)] -> IO (Int, Int, Int, Int) getResolution' xrandrEnv = do   xrandrPath <- findExecutable "xrandr" >>= \case     Just x -> return x@@ -50,12 +71,14 @@                      & filter ("connected" `T.isInfixOf`)                      & L.sortBy preferPrimary -  case headMay [(x, y, w, h) | (matchRegex resolutionRegex -> Just [(readMay -> Just w), (readMay -> Just h), (readMay -> Just x), (readMay -> Just y)]) <- fmap T.unpack connectedLines] of+  case headMay [(x, y, w, h) | (parseRegex -> Just (x, y, w, h)) <- fmap T.unpack connectedLines] of     Nothing -> throwIO $ userError "Couldn't parse xrandr output to find screen resolution.\n\n***Stdout***\n\n#{stdout}"     Just x -> return x -resolutionRegex :: Regex-resolutionRegex = mkRegex "([0-9]+)x([0-9]+)\\+([0-9]+)\\+([0-9]+)"+parseRegex :: String -> Maybe (Int, Int, Int, Int)+parseRegex context = case (context =~~ ("([0-9]+)x([0-9]+)\\+([0-9]+)\\+([0-9]+)" :: String)) :: Maybe (String, String, String, [String]) of+  Just (_before, _fullMatch, _after, [(readMay -> Just w), (readMay -> Just h), (readMay -> Just x), (readMay -> Just y)]) -> Just (x, y, w, h)+  _ -> Nothing  preferPrimary :: T.Text -> T.Text -> Ordering preferPrimary x y =
sandwich-webdriver.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.2.+-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack  name:           sandwich-webdriver-version:        0.2.3.1+version:        0.3.0.0 synopsis:       Sandwich integration with Selenium WebDriver description:    Please see the <https://codedownio.github.io/sandwich/docs/extensions/sandwich-webdriver documentation>. category:       Testing@@ -13,7 +13,7 @@ bug-reports:    https://github.com/codedownio/sandwich/issues author:         Tom McLaughlin maintainer:     tom@codedown.io-copyright:      2023 Tom McLaughlin+copyright:      2024 Tom McLaughlin license:        BSD3 license-file:   LICENSE build-type:     Simple@@ -27,25 +27,38 @@ library   exposed-modules:       Test.Sandwich.WebDriver-      Test.Sandwich.WebDriver.Class+      Test.Sandwich.WebDriver.Binaries       Test.Sandwich.WebDriver.Config+      Test.Sandwich.WebDriver.Video+      Test.Sandwich.WebDriver.Windows+  other-modules:       Test.Sandwich.WebDriver.Internal.Action-      Test.Sandwich.WebDriver.Internal.Binaries-      Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome-      Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox+      Test.Sandwich.WebDriver.Internal.Binaries.Chrome+      Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Detect+      Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Types+      Test.Sandwich.WebDriver.Internal.Binaries.Common       Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform+      Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg+      Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg.Types+      Test.Sandwich.WebDriver.Internal.Binaries.Firefox+      Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Detect+      Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Types+      Test.Sandwich.WebDriver.Internal.Binaries.Selenium+      Test.Sandwich.WebDriver.Internal.Binaries.Selenium.Types+      Test.Sandwich.WebDriver.Internal.Binaries.Xvfb+      Test.Sandwich.WebDriver.Internal.Binaries.Xvfb.Types       Test.Sandwich.WebDriver.Internal.Capabilities-      Test.Sandwich.WebDriver.Internal.Ports+      Test.Sandwich.WebDriver.Internal.Capabilities.Extra+      Test.Sandwich.WebDriver.Internal.Dependencies+      Test.Sandwich.WebDriver.Internal.OnDemand       Test.Sandwich.WebDriver.Internal.Screenshots       Test.Sandwich.WebDriver.Internal.StartWebDriver       Test.Sandwich.WebDriver.Internal.Types       Test.Sandwich.WebDriver.Internal.Util-      Test.Sandwich.WebDriver.Internal.Video       Test.Sandwich.WebDriver.Types-      Test.Sandwich.WebDriver.Video-      Test.Sandwich.WebDriver.Windows-  other-modules:-      Test.Sandwich.WebDriver.Resolution+      Test.Sandwich.WebDriver.Video.Internal+      Test.Sandwich.WebDriver.Video.Types+      Paths_sandwich_webdriver   hs-source-dirs:       src   default-extensions:@@ -74,7 +87,6 @@     , http-client     , http-client-tls     , http-conduit-    , lifted-base     , microlens     , microlens-aeson     , monad-control@@ -85,91 +97,14 @@     , random     , retry     , safe-    , safe-exceptions-    , sandwich >=0.1.0.3+    , sandwich >=0.3.0.0+    , sandwich-contexts >=0.3.0.0     , string-interpolate-    , temporary     , text     , time     , transformers-    , unordered-containers-    , vector-    , webdriver-  default-language: Haskell2010-  if os(darwin)-    hs-source-dirs:-        darwin-src-    frameworks:-        CoreGraphics-  if os(windows)-    hs-source-dirs:-        windows-src-  if os(linux)-    hs-source-dirs:-        linux-src-    build-depends:-        regex-compat-  if !os(windows)-    build-depends:-        unix-  if !os(windows)-    other-modules:-        Test.Sandwich.WebDriver.Internal.StartWebDriver.Xvfb-    hs-source-dirs:-        unix-src--executable sandwich-webdriver-exe-  main-is: Main.hs-  other-modules:-      Simple-      Paths_sandwich_webdriver-  hs-source-dirs:-      app-  default-extensions:-      FlexibleContexts-      FlexibleInstances-      LambdaCase-      MultiParamTypeClasses-      MultiWayIf-      NamedFieldPuns-      NumericUnderscores-      OverloadedStrings-      QuasiQuotes-      RecordWildCards-      ScopedTypeVariables-      ViewPatterns-  ghc-options: -threaded -rtsopts -with-rtsopts=-N-  build-depends:-      aeson-    , base <5-    , bytestring-    , containers-    , data-default-    , directory-    , exceptions-    , filepath-    , http-client-    , http-client-tls-    , http-conduit-    , lifted-base-    , microlens-    , microlens-aeson-    , monad-control-    , monad-logger-    , mtl-    , network-    , process-    , random-    , retry-    , safe-    , safe-exceptions-    , sandwich >=0.1.0.3-    , sandwich-webdriver-    , string-interpolate-    , temporary-    , text-    , time-    , transformers+    , unliftio+    , unliftio-core     , unordered-containers     , vector     , webdriver@@ -192,10 +127,17 @@     hs-source-dirs:         linux-src     build-depends:-        regex-compat+        regex-tdfa   if !os(windows)     build-depends:         unix+  if !os(windows)+    other-modules:+        Test.Sandwich.WebDriver.Internal.StartWebDriver.Xvfb+    hs-source-dirs:+        unix-src+    build-depends:+        temporary  test-suite sandwich-webdriver-test   type: exitcode-stdio-1.0@@ -230,7 +172,6 @@     , http-client     , http-client-tls     , http-conduit-    , lifted-base     , microlens     , microlens-aeson     , monad-control@@ -241,15 +182,15 @@     , random     , retry     , safe-    , safe-exceptions-    , sandwich >=0.1.0.3+    , sandwich >=0.3.0.0+    , sandwich-contexts >=0.3.0.0     , sandwich-webdriver     , string-interpolate-    , temporary     , text     , time     , transformers     , unliftio+    , unliftio-core     , unordered-containers     , vector     , webdriver@@ -272,7 +213,7 @@     hs-source-dirs:         linux-src     build-depends:-        regex-compat+        regex-tdfa   if !os(windows)     build-depends:         unix
src/Test/Sandwich/WebDriver.hs view
@@ -1,15 +1,25 @@-{-# LANGUAGE TypeOperators #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-} +{-|+Introduce [WebDriver](https://www.selenium.dev/documentation/webdriver/) servers and sessions.+-}+ module Test.Sandwich.WebDriver (   -- * Introducing a WebDriver server   introduceWebDriver-  , introduceWebDriverOptions-  , addCommandLineOptionsToWdOptions+  , introduceWebDriverViaNix+  , introduceWebDriverViaNix' +  -- * Non-Nix dependency fetching+  -- | When you aren't using Nix, these types specify how to obtain the necessary dependencies.+  , defaultWebDriverDependencies+  , WebDriverDependencies(..)+   -- * Running an example in a given session+  -- | Once you have a 'WebDriver' in context, you can run one or more sessions.+  -- Each session will open an independent browser instance.   , withSession   , withSession1   , withSession2@@ -24,78 +34,165 @@    -- * Lower-level allocation functions   , allocateWebDriver-  , allocateWebDriver'   , cleanupWebDriver-  , cleanupWebDriver'+  , introduceBrowserDependenciesViaNix+  , introduceBrowserDependenciesViaNix'+  , introduceWebDriver'+  , addCommandLineOptionsToWdOptions +  -- * Context types+  -- ** WebDriver+  , webdriver+  , WebDriver+  , HasWebDriverContext+  -- ** WebDriverSession+  , webdriverSession+  , WebDriverSession+  , HasWebDriverSessionContext++  -- * Shorthands+  -- | These are used to make type signatures shorter.+  , BaseMonad+  , ContextWithBaseDeps+  , ContextWithWebdriverDeps+  , WebDriverMonad+  , WebDriverSessionMonad++  -- * On demand options+  , OnDemandOptions+  , defaultOnDemandOptions+   -- * Re-exports-  , module Test.Sandwich.WebDriver.Class   , module Test.Sandwich.WebDriver.Config-  , module Test.Sandwich.WebDriver.Types   ) where -import Control.Applicative-import Control.Concurrent.MVar.Lifted+import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class-import Control.Monad.Logger import Control.Monad.Reader+import Control.Monad.Trans.Control (MonadBaseControl) import Data.IORef+import qualified Data.List as L import qualified Data.Map as M import Data.Maybe import Data.String.Interpolate import Test.Sandwich-import Test.Sandwich.Internal-import Test.Sandwich.WebDriver.Class+import Test.Sandwich.Contexts.Files+import Test.Sandwich.Contexts.Nix+import Test.Sandwich.WebDriver.Binaries import Test.Sandwich.WebDriver.Config import Test.Sandwich.WebDriver.Internal.Action+import Test.Sandwich.WebDriver.Internal.Dependencies import Test.Sandwich.WebDriver.Internal.StartWebDriver import Test.Sandwich.WebDriver.Internal.Types import Test.Sandwich.WebDriver.Types+import Test.Sandwich.WebDriver.Video (recordVideoIfConfigured) import qualified Test.WebDriver as W import qualified Test.WebDriver.Config as W import qualified Test.WebDriver.Session as W+import UnliftIO.MVar  --- | This is the main 'introduce' method for creating a WebDriver.-introduceWebDriver :: (BaseMonadContext m context) => WdOptions -> SpecFree (LabelValue "webdriver" WebDriver :> context) m () -> SpecFree context m ()-introduceWebDriver wdOptions = introduce "Introduce WebDriver session" webdriver (allocateWebDriver wdOptions) cleanupWebDriver+-- | Introduce a 'WebDriver', using the given 'WebDriverDependencies'.+-- A good default is 'defaultWebDriverDependencies'.+introduceWebDriver :: forall context m. (+  BaseMonad m context, HasSomeCommandLineOptions context+  )+  -- | How to obtain dependencies+  => WebDriverDependencies+  -> WdOptions+  -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()+introduceWebDriver wdd wdOptions = introduceWebDriver' wdd alloc wdOptions+  where+    alloc wdOptions' = do+      clo <- getSomeCommandLineOptions+      allocateWebDriver (addCommandLineOptionsToWdOptions clo wdOptions') onDemandOptions --- | Same as introduceWebDriver, but merges command line options into the 'WdOptions'.-introduceWebDriverOptions :: forall a context m. (BaseMonadContext m context, HasCommandLineOptions context a)-  => WdOptions -> SpecFree (LabelValue "webdriver" WebDriver :> context) m () -> SpecFree context m ()-introduceWebDriverOptions wdOptions = introduce "Introduce WebDriver session" webdriver alloc cleanupWebDriver-  where alloc = do-          clo <- getCommandLineOptions-          allocateWebDriver (addCommandLineOptionsToWdOptions @a clo wdOptions)+    onDemandOptions = OnDemandOptions {+      ffmpegToUse = webDriverFfmpeg wdd+      , xvfbToUse = xvfbDependenciesSpecXvfb $ webDriverXvfb wdd+      } +-- | Introduce a 'WebDriver' using the current 'NixContext'.+-- This will pull everything required from the configured Nixpkgs snapshot.+introduceWebDriverViaNix :: forall m context. (+  BaseMonad m context, HasSomeCommandLineOptions context, HasNixContext context+  )+  -- | Options+  => WdOptions+  -> SpecFree (ContextWithWebdriverDeps context) m ()+  -> SpecFree context m ()+introduceWebDriverViaNix = introduceWebDriverViaNix' (defaultNodeOptions { nodeOptionsVisibilityThreshold = defaultFileContextVisibilityThreshold })++-- | Same as 'introduceWebDriverViaNix', but allows passing custom 'NodeOptions'.+introduceWebDriverViaNix' :: forall m context. (+  BaseMonad m context, HasSomeCommandLineOptions context, HasNixContext context+  )+  => NodeOptions+  -- | Options+  -> WdOptions+  -> SpecFree (ContextWithWebdriverDeps context) m ()+  -> SpecFree context m ()+introduceWebDriverViaNix' nodeOptions wdOptions =+  introduceFileViaNixPackage'' @"selenium.jar" nodeOptions "selenium-server-standalone" (findFirstFile (return . (".jar" `L.isSuffixOf`)))+  . introduceBinaryViaNixPackage' @"java" nodeOptions "jre"+  . introduceBrowserDependenciesViaNix' nodeOptions+  . introduce "Introduce WebDriver session" webdriver alloc cleanupWebDriver+  where+    alloc = do+      clo <- getSomeCommandLineOptions++      nc <- getContext nixContext+      let onDemandOptions = OnDemandOptions {+            ffmpegToUse = UseFfmpegFromNixpkgs nc+            , xvfbToUse = UseXvfbFromNixpkgs nc+            }++      allocateWebDriver (addCommandLineOptionsToWdOptions clo wdOptions) onDemandOptions++-- | Same as 'introduceWebDriver', but with a controllable allocation callback.+introduceWebDriver' :: forall m context. (+  BaseMonad m context+  )+  -- | Dependencies+  => WebDriverDependencies+  -> (WdOptions -> ExampleT (ContextWithBaseDeps context) m WebDriver)+  -> WdOptions+  -> SpecFree (ContextWithWebdriverDeps context) m () -> SpecFree context m ()+introduceWebDriver' (WebDriverDependencies {..}) alloc wdOptions =+  introduce "Introduce selenium.jar" (mkFileLabel @"selenium.jar") ((EnvironmentFile <$>) $ obtainSelenium webDriverDependencySelenium) (const $ return ())+  . (case webDriverDependencyJava of Nothing -> introduceBinaryViaEnvironment @"java"; Just p -> introduceFile @"java" p)+  . introduce "Introduce browser dependencies" browserDependencies (getBrowserDependencies webDriverDependencyBrowser) (const $ return ())+  . introduce "Introduce WebDriver session" webdriver (alloc wdOptions) cleanupWebDriver+ -- | Allocate a WebDriver using the given options.-allocateWebDriver :: (HasBaseContext context, BaseMonad m) => WdOptions -> ExampleT context m WebDriver-allocateWebDriver wdOptions = do-  debug "Beginning allocateWebDriver"+allocateWebDriver :: (+  BaseMonad m context+  , HasFile context "java", HasFile context "selenium.jar", HasBrowserDependencies context+  )+  -- | Options+  => WdOptions+  -> OnDemandOptions+  -> ExampleT context m WebDriver+allocateWebDriver wdOptions onDemandOptions = do   dir <- fromMaybe "/tmp" <$> getCurrentFolder-  startWebDriver wdOptions dir---- | Allocate a WebDriver using the given options and putting logs under the given path.-allocateWebDriver' :: FilePath -> WdOptions -> IO WebDriver-allocateWebDriver' runRoot wdOptions = do-  runNoLoggingT $ startWebDriver wdOptions runRoot+  startWebDriver wdOptions onDemandOptions dir  -- | Clean up the given WebDriver.-cleanupWebDriver :: (HasBaseContext context, BaseMonad m) => WebDriver -> ExampleT context m ()+cleanupWebDriver :: (BaseMonad m context) => WebDriver -> ExampleT context m () cleanupWebDriver sess = do   closeAllSessions sess   stopWebDriver sess --- | Clean up the given WebDriver without logging.-cleanupWebDriver' :: WebDriver -> IO ()-cleanupWebDriver' sess = do-  runNoLoggingT $ do-    closeAllSessions sess-    stopWebDriver sess- -- | Run a given example using a given Selenium session.-withSession :: forall m context a. WebDriverMonad m context => Session -> ExampleT (ContextWithSession context) m a -> ExampleT context m a-withSession session (ExampleT readerMonad) = do+withSession :: forall m context a. (+  MonadMask m, MonadBaseControl IO m+  , HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context+  )+  -- | Session to run+  => Session+  -> ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a+  -> ExampleT context m a+withSession session action = do   WebDriver {..} <- getContext webdriver   -- Create new session if necessary (this can throw an exception)   sess <- modifyMVar wdSessionMap $ \sessionMap -> case M.lookup session sessionMap of@@ -111,51 +208,43 @@    -- Not used for now, but previous libraries have use a finally to grab the final session on exception.   -- We could do the same here, but it's not clear that it's needed.-  let f :: m a -> m a = id+  -- let f :: m a -> m a = id -  ExampleT (withReaderT (\ctx -> LabelValue (session, ref) :> ctx) $ mapReaderT (mapLoggingT f) readerMonad)+  pushContext webdriverSession (session, ref) $+    recordVideoIfConfigured session action --- | Convenience function. 'withSession1' = 'withSession' "session1"-withSession1 :: WebDriverMonad m context => ExampleT (ContextWithSession context) m a -> ExampleT context m a+-- | Convenience function. @withSession1 = withSession "session1"@.+withSession1 :: (+  MonadMask m, MonadBaseControl IO m+  , HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context+  )+  -- | Wrapped action+  => ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a+  -> ExampleT context m a withSession1 = withSession "session1" --- | Convenience function. 'withSession2' = 'withSession' "session2"-withSession2 :: WebDriverMonad m context => ExampleT (ContextWithSession context) m a -> ExampleT context m a+-- | Convenience function. @withSession2 = withSession "session2"@.+withSession2 :: (+  MonadMask m, MonadBaseControl IO m+  , HasBaseContext context, HasSomeCommandLineOptions context, WebDriverMonad m context+  )+  -- | Wrapped action+  => ExampleT (LabelValue "webdriverSession" WebDriverSession :> context) m a+  -> ExampleT context m a withSession2 = withSession "session2" --- | Get all existing session names-getSessions :: (WebDriverMonad m context, MonadReader context m, HasLabel context "webdriver" WebDriver) => m [Session]+-- | Get all existing session names.+getSessions :: (MonadReader context m, WebDriverMonad m context) => m [Session] getSessions = do   WebDriver {..} <- getContext webdriver   M.keys <$> liftIO (readMVar wdSessionMap)  -- | Merge the options from the 'CommandLineOptions' into some 'WdOptions'.-addCommandLineOptionsToWdOptions :: CommandLineOptions a -> WdOptions -> WdOptions-addCommandLineOptionsToWdOptions (CommandLineOptions {optWebdriverOptions=(CommandLineWebdriverOptions {..})}) wdOptions@(WdOptions {..}) = wdOptions {-  capabilities = case optFirefox of-    Just UseFirefox -> firefoxCapabilities fbp-    Just UseChrome -> chromeCapabilities cbp-    Nothing -> case cbp of-      Just p -> chromeCapabilities (Just p)-      Nothing -> case fbp of-        Just p -> firefoxCapabilities (Just p)-        Nothing -> capabilities--  , runMode = case optDisplay of-      Nothing -> runMode-      Just Headless -> RunHeadless defaultHeadlessConfig-      Just Xvfb -> RunInXvfb (defaultXvfbConfig { xvfbStartFluxbox = optFluxbox })-      Just Current -> Normal--  , seleniumToUse = maybe seleniumToUse UseSeleniumAt optSeleniumJar--  , chromeBinaryPath = cbp-  , chromeDriverToUse = maybe chromeDriverToUse UseChromeDriverAt optChromeDriver--  , firefoxBinaryPath = fbp-  , geckoDriverToUse = maybe geckoDriverToUse UseGeckoDriverAt optGeckoDriver+addCommandLineOptionsToWdOptions :: SomeCommandLineOptions -> WdOptions -> WdOptions+addCommandLineOptionsToWdOptions (SomeCommandLineOptions (CommandLineOptions {optWebdriverOptions=(CommandLineWebdriverOptions {..})})) wdOptions@(WdOptions {..}) = wdOptions {+  runMode = case optDisplay of+    Nothing -> runMode+    Just Headless -> RunHeadless defaultHeadlessConfig+    Just Xvfb -> RunInXvfb (defaultXvfbConfig { xvfbStartFluxbox = optFluxbox })+    Just Current -> Normal   }--  where-    cbp = optChromeBinary <|> chromeBinaryPath-    fbp = optFirefoxBinary <|> firefoxBinaryPath
+ src/Test/Sandwich/WebDriver/Binaries.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE Rank2Types #-}++{-|+Obtain various binaries you might need for WebDriver testing.+-}++module Test.Sandwich.WebDriver.Binaries (+  -- * Selenium+  obtainSelenium+  , SeleniumToUse(..)++  -- * Chrome+  , obtainChrome+  , ChromeToUse(..)+  , ChromeVersion(..)++  -- * Chrome driver+  , obtainChromeDriver+  , ChromeDriverToUse(..)+  , ChromeDriverVersion(..)++  -- * Firefox+  , obtainFirefox+  , FirefoxToUse(..)++  -- * Geckodriver+  , obtainGeckoDriver+  , GeckoDriverToUse(..)+  , GeckoDriverVersion(..)++  -- * Ffmpeg+  , obtainFfmpeg+  , FfmpegToUse(..)++  -- * Xvfb+  , obtainXvfb+  , XvfbDependenciesSpec(..)+  , XvfbToUse(..)+  , FluxboxToUse(..)+  ) where++import Test.Sandwich.WebDriver.Internal.Binaries.Chrome+import Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg+import Test.Sandwich.WebDriver.Internal.Binaries.Firefox+import Test.Sandwich.WebDriver.Internal.Binaries.Selenium+import Test.Sandwich.WebDriver.Internal.Binaries.Xvfb
− src/Test/Sandwich/WebDriver/Class.hs
@@ -1,15 +0,0 @@--module Test.Sandwich.WebDriver.Class (-  -- * The main WebDriver type-  WebDriver-  , getWdOptions-  , getDisplayNumber-  , getXvfbSession-  , getWebDriverName--  -- * The Xvfb session-  , XvfbSession(..)-  ) where---import Test.Sandwich.WebDriver.Internal.Types
src/Test/Sandwich/WebDriver/Config.hs view
@@ -1,54 +1,53 @@ +-- | Configuration types for WebDriver servers, Xvfb mode, browser capabilities, etc.+ module Test.Sandwich.WebDriver.Config (   -- * Main options   WdOptions   , defaultWdOptions   , runMode-  , seleniumToUse-  , chromeBinaryPath-  , chromeDriverToUse-  , firefoxBinaryPath-  , geckoDriverToUse   , capabilities   , httpManager   , httpRetryCount   , saveSeleniumMessageHistory -  -- * Run mode constructors-  , RunMode(..)+  -- * Accessors for the 'WebDriver' context+  , getWdOptions+  , getDisplayNumber+  , getDownloadDirectory+  , getWebDriverName+  , getXvfbSession -  -- ** Xvfb mode+  -- * Xvfb mode   , XvfbConfig   , defaultXvfbConfig   , xvfbResolution   , xvfbStartFluxbox -  -- ** Headless mode+  -- * Headless mode   , HeadlessConfig   , defaultHeadlessConfig   , headlessResolution -  -- * Binary fetching options-  , SeleniumToUse(..)-  , ChromeDriverToUse(..)-  , GeckoDriverToUse(..)--  -- * Miscellaneous constructors-  , WhenToSave(..)--  -- * Manually obtaining binaries-  , obtainSelenium-  , obtainChromeDriver-  , obtainGeckoDriver-   -- * Browser capabilities   , chromeCapabilities   , headlessChromeCapabilities   , firefoxCapabilities   , headlessFirefoxCapabilities +  -- * Types+  -- ** BrowserDependencies+  , browserDependencies+  , BrowserDependenciesSpec(..)+  , BrowserDependencies(..)+  , HasBrowserDependencies+  -- ** Xvfb+  , XvfbSession(..)+  -- ** Miscellaneous+  , WhenToSave(..)+  , RunMode(..)   ) where -import Test.Sandwich.WebDriver.Internal.Binaries import Test.Sandwich.WebDriver.Internal.Capabilities+import Test.Sandwich.WebDriver.Internal.Dependencies import Test.Sandwich.WebDriver.Internal.Types
src/Test/Sandwich/WebDriver/Internal/Action.hs view
@@ -2,24 +2,24 @@  module Test.Sandwich.WebDriver.Internal.Action where -import Control.Concurrent.MVar.Lifted-import Control.Exception.Safe import Control.Monad import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Control.Monad.Logger-import Control.Monad.Reader-import Control.Monad.Trans.Control (MonadBaseControl) import qualified Data.Map as M import Data.String.Interpolate import GHC.Stack import Test.Sandwich import Test.Sandwich.WebDriver.Internal.Types import Test.Sandwich.WebDriver.Internal.Util+import Test.Sandwich.WebDriver.Types import qualified Test.WebDriver as W+import UnliftIO.Concurrent+import UnliftIO.Exception  --- | Close the given sessions-closeSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => Session -> WebDriver -> m ()+-- | Close the given session.+closeSession :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => Session -> WebDriver -> m () closeSession session (WebDriver {wdSessionMap}) = do   toClose <- modifyMVar wdSessionMap $ \sessionMap ->     case M.lookup session sessionMap of@@ -28,8 +28,8 @@    whenJust toClose $ \sess -> liftIO $ W.runWD sess W.closeSession --- | Close all sessions except those listed-closeAllSessionsExcept :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => [Session] -> WebDriver -> m ()+-- | Close all sessions except those listed.+closeAllSessionsExcept :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => [Session] -> WebDriver -> m () closeAllSessionsExcept toKeep (WebDriver {wdSessionMap}) = do   toClose <- modifyMVar wdSessionMap $ return . M.partitionWithKey (\name _ -> name `elem` toKeep) @@ -37,12 +37,14 @@     catch (liftIO $ W.runWD sess W.closeSession)           (\(e :: SomeException) -> warn [i|Failed to destroy session '#{name}': '#{e}'|]) --- | Close all sessions-closeAllSessions :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m) => WebDriver -> m ()+-- | Close all sessions.+closeAllSessions :: (HasCallStack, MonadLogger m, MonadUnliftIO m) => WebDriver -> m () closeAllSessions = closeAllSessionsExcept [] --- | Close the current session-closeCurrentSession :: (HasCallStack, MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadCatch m, MonadReader context m, HasLabel context "webdriver" WebDriver, HasLabel context "webdriverSession" WebDriverSession) => m ()+-- | Close the current session.+closeCurrentSession :: (+  MonadLogger m, WebDriverSessionMonad m context+  ) => m () closeCurrentSession = do   webDriver <- getContext webdriver   (session, _) <- getContext webdriverSession
− src/Test/Sandwich/WebDriver/Internal/Binaries.hs
@@ -1,200 +0,0 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE Rank2Types #-}--module Test.Sandwich.WebDriver.Internal.Binaries (-  obtainSelenium-  , obtainChromeDriver-  , obtainGeckoDriver-  , downloadSeleniumIfNecessary-  , downloadChromeDriverIfNecessary-  ) where--import Control.Exception-import Control.Monad-import Control.Monad.Catch-import Control.Monad.IO.Class-import Control.Monad.Logger-import Control.Monad.Trans.Control (MonadBaseControl)-import Control.Monad.Trans.Except-import Data.String.Interpolate-import qualified Data.Text as T-import GHC.Stack-import System.Directory-import System.Exit-import System.FilePath-import System.IO.Temp-import System.Process-import Test.Sandwich.Expectations-import Test.Sandwich.Logging-import Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome-import Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox-import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform-import Test.Sandwich.WebDriver.Internal.Types-import Test.Sandwich.WebDriver.Internal.Util---type Constraints m = (-  HasCallStack-  , MonadLogger m-  , MonadIO m-  , MonadBaseControl IO m-  , MonadMask m-  )---- * Obtaining binaries--defaultSeleniumJarUrl :: String-defaultSeleniumJarUrl = "https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar"---- TODO: remove curl dependencies here---- | Manually obtain a Selenium server JAR file, according to the 'SeleniumToUse' policy,--- storing it under the provided 'FilePath' if necessary and returning the exact path.-obtainSelenium :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> SeleniumToUse -> m (Either T.Text FilePath)-obtainSelenium toolsDir (DownloadSeleniumFrom url) = do-  let path = [i|#{toolsDir}/selenium-server-standalone.jar|]-  unlessM (liftIO $ doesFileExist path) $-    curlDownloadToPath url path-  return $ Right path-obtainSelenium toolsDir DownloadSeleniumDefault = do-  let path = [i|#{toolsDir}/selenium-server-standalone-3.141.59.jar|]-  unlessM (liftIO $ doesFileExist path) $-    curlDownloadToPath defaultSeleniumJarUrl path-  return $ Right path-obtainSelenium _ (UseSeleniumAt path) = liftIO (doesFileExist path) >>= \case-  False -> return $ Left [i|Path '#{path}' didn't exist|]-  True -> return $ Right path---- | Manually obtain a chromedriver binary, according to the 'ChromeDriverToUse' policy,--- storing it under the provided 'FilePath' if necessary and returning the exact path.-obtainChromeDriver :: (-  MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadMask m-  ) => FilePath -> ChromeDriverToUse -> m (Either T.Text FilePath)-obtainChromeDriver toolsDir (DownloadChromeDriverFrom url) = do-  let path = [i|#{toolsDir}/#{chromeDriverExecutable}|]-  unlessM (liftIO $ doesFileExist path) $-    curlDownloadToPath url path-  return $ Right path-obtainChromeDriver toolsDir (DownloadChromeDriverVersion chromeDriverVersion) = runExceptT $ do-  let path = getChromeDriverPath toolsDir chromeDriverVersion-  liftIO (doesFileExist path) >>= \case-    True -> return path-    False -> do-      let downloadPath = getChromeDriverDownloadUrl chromeDriverVersion detectPlatform-      ExceptT $ downloadAndUnzipToPath downloadPath path-      return path-obtainChromeDriver toolsDir (DownloadChromeDriverAutodetect maybeChromePath) = runExceptT $ do-  version <- ExceptT $ liftIO $ getChromeDriverVersion maybeChromePath-  ExceptT $ obtainChromeDriver toolsDir (DownloadChromeDriverVersion version)-obtainChromeDriver _ (UseChromeDriverAt path) = liftIO (doesFileExist path) >>= \case-  False -> return $ Left [i|Path '#{path}' didn't exist|]-  True -> return $ Right path---- | Manually obtain a geckodriver binary, according to the 'GeckoDriverToUse' policy,--- storing it under the provided 'FilePath' if necessary and returning the exact path.-obtainGeckoDriver :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => FilePath -> GeckoDriverToUse -> m (Either T.Text FilePath)-obtainGeckoDriver toolsDir (DownloadGeckoDriverFrom url) = do-  let path = [i|#{toolsDir}/#{geckoDriverExecutable}|]-  unlessM (liftIO $ doesFileExist path) $-    curlDownloadToPath url path-  return $ Right path-obtainGeckoDriver toolsDir (DownloadGeckoDriverVersion geckoDriverVersion) = runExceptT $ do-  let path = getGeckoDriverPath toolsDir geckoDriverVersion-  liftIO (doesFileExist path) >>= \case-    True -> return path-    False -> do-      let downloadPath = getGeckoDriverDownloadUrl geckoDriverVersion detectPlatform-      ExceptT $ downloadAndUntarballToPath downloadPath path-      return path-obtainGeckoDriver toolsDir (DownloadGeckoDriverAutodetect maybeFirefoxPath) = runExceptT $ do-  version <- ExceptT $ liftIO $ getGeckoDriverVersion maybeFirefoxPath-  ExceptT $ obtainGeckoDriver toolsDir (DownloadGeckoDriverVersion version)-obtainGeckoDriver _ (UseGeckoDriverAt path) = liftIO (doesFileExist path) >>= \case-  False -> return $ Left [i|Path '#{path}' didn't exist|]-  True -> return $ Right path---- * Lower level helpers---downloadSeleniumIfNecessary :: Constraints m => FilePath -> m (Either T.Text FilePath)-downloadSeleniumIfNecessary toolsDir = leftOnException' $ do-  let seleniumPath = [i|#{toolsDir}/selenium-server.jar|]-  liftIO (doesFileExist seleniumPath) >>= flip unless (downloadSelenium seleniumPath)-  return seleniumPath-  where-    downloadSelenium :: Constraints m => FilePath -> m ()-    downloadSelenium seleniumPath = void $ do-      info [i|Downloading selenium-server.jar to #{seleniumPath}|]-      curlDownloadToPath defaultSeleniumJarUrl seleniumPath--downloadChromeDriverIfNecessary' :: Constraints m => FilePath -> ChromeDriverVersion -> m (Either T.Text FilePath)-downloadChromeDriverIfNecessary' toolsDir chromeDriverVersion = runExceptT $ do-  let chromeDriverPath = getChromeDriverPath toolsDir chromeDriverVersion--  unlessM (liftIO $ doesFileExist chromeDriverPath) $ do-    let downloadPath = getChromeDriverDownloadUrl chromeDriverVersion detectPlatform-    ExceptT $ downloadAndUnzipToPath downloadPath chromeDriverPath--  return chromeDriverPath--downloadChromeDriverIfNecessary :: Constraints m => Maybe FilePath -> FilePath -> m (Either T.Text FilePath)-downloadChromeDriverIfNecessary maybeChromePath toolsDir = runExceptT $ do-  chromeDriverVersion <- ExceptT $ liftIO $ getChromeDriverVersion maybeChromePath-  ExceptT $ downloadChromeDriverIfNecessary' toolsDir chromeDriverVersion--getChromeDriverPath :: FilePath -> ChromeDriverVersion -> FilePath-getChromeDriverPath toolsDir (ChromeDriverVersionTuple (w, x, y, z)) = [i|#{toolsDir}/chromedrivers/#{w}.#{x}.#{y}.#{z}/#{chromeDriverExecutable}|]-getChromeDriverPath toolsDir (ChromeDriverVersionExactUrl (w, x, y, z) _) = [i|#{toolsDir}/chromedrivers/#{w}.#{x}.#{y}.#{z}/#{chromeDriverExecutable}|]--getGeckoDriverPath :: FilePath -> GeckoDriverVersion -> FilePath-getGeckoDriverPath toolsDir (GeckoDriverVersion (x, y, z)) = [i|#{toolsDir}/geckodrivers/#{x}.#{y}.#{z}/#{geckoDriverExecutable}|]--chromeDriverExecutable :: T.Text-chromeDriverExecutable = case detectPlatform of-  Windows -> "chromedriver.exe"-  _ -> "chromedriver"--geckoDriverExecutable :: T.Text-geckoDriverExecutable = case detectPlatform of-  Windows -> "geckodriver.exe"-  _ -> "geckodriver"--downloadAndUnzipToPath :: (MonadIO m, MonadBaseControl IO m, MonadLogger m, MonadMask m) => T.Text -> FilePath -> m (Either T.Text ())-downloadAndUnzipToPath downloadPath localPath = leftOnException' $ do-  info [i|Downloading #{downloadPath} to #{localPath}|]-  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)-  withSystemTempDirectory "sandwich-webdriver-tool-download" $ \dir -> do-    curlDownloadToPath (T.unpack downloadPath) (dir </> "temp.zip")--    createProcessWithLogging ((proc "unzip" ["temp.zip", "-d", "unzipped"]) { cwd = Just dir })-      >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)-    let unzipped = dir </> "unzipped"--    executables <- (filter (/= "") . T.splitOn "\n" . T.pack) <$> readCreateProcessWithLogging (proc "find" [unzipped, "-executable", "-type", "f"]) ""-    case executables of-      [] -> liftIO $ throwIO $ userError [i|No executable found in file downloaded from #{downloadPath}|]-      [x] -> do-        liftIO $ copyFile (T.unpack x) localPath-        createProcessWithLogging (shell [i|chmod u+x #{localPath}|])-          >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)-      xs -> liftIO $ throwIO $ userError [i|Found multiple executable found in file downloaded from #{downloadPath}: #{xs}|]--downloadAndUntarballToPath :: (MonadIO m, MonadBaseControl IO m, MonadLogger m, MonadThrow m) => T.Text -> FilePath -> m (Either T.Text ())-downloadAndUntarballToPath downloadPath localPath = leftOnException' $ do-  info [i|Downloading #{downloadPath} to #{localPath}|]-  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)-  createProcessWithLogging (shell [i|wget -qO- #{downloadPath} | tar xvz  -C #{takeDirectory localPath}|])-    >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)-  createProcessWithLogging (shell [i|chmod u+x #{localPath}|])-    >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)--curlDownloadToPath :: (MonadIO m, MonadLogger m, MonadBaseControl IO m, MonadThrow m) => String -> FilePath -> m ()-curlDownloadToPath downloadPath localPath = do-  info [i|Downloading #{downloadPath} to #{localPath}|]-  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)-  p <- createProcessWithLogging (proc "curl" [downloadPath, "-o", localPath, "-s"])-  liftIO (waitForProcess p) >>= (`shouldBe` ExitSuccess)--unlessM :: Monad m => m Bool -> m () -> m ()-unlessM b s = b >>= (\t -> unless t s)
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Chrome.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Chrome (+  obtainChrome+  , obtainChromeDriver++  -- * Lower-level+  , downloadChromeDriverIfNecessary++  -- * Types+  , ChromeToUse(..)+  , ChromeDriverToUse(..)+  , ChromeVersion(..)+  , ChromeDriverVersion(..)+  ) where++import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Control.Monad.Trans.Except+import Data.String.Interpolate+import Data.Text as T+import GHC.Stack+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Detect+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Types+import Test.Sandwich.WebDriver.Internal.Binaries.Common+import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform+import UnliftIO.Directory+++type Constraints m = (+  HasCallStack+  , MonadLogger m+  , MonadUnliftIO m+  )++-- | Manually obtain a chrome binary, according to the 'ChromeToUse' policy,+obtainChrome :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLogger m+  ) => ChromeToUse -> m (Either T.Text FilePath)+obtainChrome UseChromeFromPath = do+  findExecutable "google-chrome" >>= \case+    Just p -> return $ Right p+    Nothing -> findExecutable "google-chrome-stable" >>= \case+      Just p -> return $ Right p+      Nothing -> expectationFailure [i|Couldn't find either "google-chrome" or "google-chrome-stable" on the PATH|]+obtainChrome (UseChromeAt p) = doesFileExist p >>= \case+  False -> return $ Left [i|Path '#{p}' didn't exist|]+  True -> return $ Right p+obtainChrome (UseChromeFromNixpkgs nixContext) =+  Right <$> getBinaryViaNixPackage' @"google-chrome-stable" nixContext "google-chrome"++-- | Manually obtain a @chromedriver@ binary, according to the 'ChromeDriverToUse' policy.+obtainChromeDriver :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLogger m+  )+  -- | How to obtain @chromedriver@+  => ChromeDriverToUse+  -> m (Either T.Text FilePath)+obtainChromeDriver (DownloadChromeDriverFrom toolsDir url) = do+  let path = [i|#{toolsDir}/#{chromeDriverExecutable}|]+  unlessM (liftIO $ doesFileExist path) $+    curlDownloadToPath url path+  return $ Right path+obtainChromeDriver (DownloadChromeDriverVersion toolsDir chromeDriverVersion) = runExceptT $ do+  let path = getChromeDriverPath toolsDir chromeDriverVersion+  liftIO (doesFileExist path) >>= \case+    True -> return path+    False -> do+      let downloadPath = getChromeDriverDownloadUrl chromeDriverVersion detectPlatform+      ExceptT $ downloadAndUnzipToPath downloadPath path+      return path+obtainChromeDriver (DownloadChromeDriverAutodetect toolsDir chromePath) = runExceptT $ do+  version <- ExceptT $ liftIO $ getChromeDriverVersion chromePath+  ExceptT $ obtainChromeDriver (DownloadChromeDriverVersion toolsDir version)+obtainChromeDriver (UseChromeDriverAt path) = doesFileExist path >>= \case+  False -> return $ Left [i|Path '#{path}' didn't exist|]+  True -> return $ Right path+obtainChromeDriver (UseChromeDriverFromNixpkgs nixContext) =+  Right <$> getBinaryViaNixPackage' @"chromedriver" nixContext "chromedriver"+++downloadChromeDriverIfNecessary' :: Constraints m => FilePath -> ChromeDriverVersion -> m (Either T.Text FilePath)+downloadChromeDriverIfNecessary' toolsDir chromeDriverVersion = runExceptT $ do+  let chromeDriverPath = getChromeDriverPath toolsDir chromeDriverVersion++  unlessM (liftIO $ doesFileExist chromeDriverPath) $ do+    let downloadPath = getChromeDriverDownloadUrl chromeDriverVersion detectPlatform+    ExceptT $ downloadAndUnzipToPath downloadPath chromeDriverPath++  return chromeDriverPath++downloadChromeDriverIfNecessary :: Constraints m => FilePath -> FilePath -> m (Either T.Text FilePath)+downloadChromeDriverIfNecessary chromePath toolsDir = runExceptT $ do+  chromeDriverVersion <- ExceptT $ liftIO $ getChromeDriverVersion chromePath+  ExceptT $ downloadChromeDriverIfNecessary' toolsDir chromeDriverVersion++getChromeDriverPath :: FilePath -> ChromeDriverVersion -> FilePath+getChromeDriverPath toolsDir (ChromeDriverVersionTuple (w, x, y, z)) =+  [i|#{toolsDir}/chromedrivers/#{w}.#{x}.#{y}.#{z}/#{chromeDriverExecutable}|]+getChromeDriverPath toolsDir (ChromeDriverVersionExactUrl (w, x, y, z) _) =+  [i|#{toolsDir}/chromedrivers/#{w}.#{x}.#{y}.#{z}/#{chromeDriverExecutable}|]++chromeDriverExecutable :: T.Text+chromeDriverExecutable = case detectPlatform of+  Windows -> "chromedriver.exe"+  _ -> "chromedriver"
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Chrome/Detect.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Detect (+  detectChromeVersion+  , getChromeDriverVersion+  , getChromeDriverDownloadUrl++  , findChromeInEnvironment+  ) where++import Control.Exception+import Control.Monad.IO.Class+import Control.Monad.Trans.Except+import Data.Aeson as A+import qualified Data.ByteString.Lazy as LB+import Data.Function+import Data.Map as M hiding (mapMaybe)+import Data.Maybe (mapMaybe)+import Data.String.Interpolate+import Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TL+import GHC.Generics+import Network.HTTP.Client+import Network.HTTP.Conduit (simpleHttp)+import Safe+import System.Directory (findExecutable)+import System.Exit+import qualified System.Info as SI+import System.Process+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Types+import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform+import Test.Sandwich.WebDriver.Internal.Util+++data PlatformAndUrl = PlatformAndUrl {+  platform :: Text+  , url :: Text+  } deriving (Show, Generic, FromJSON, ToJSON)++data Version = Version {+  version :: Text+  , revision :: Text+  , downloads :: Map Text [PlatformAndUrl]+  } deriving (Show, Generic, FromJSON, ToJSON)++data JsonResponse = JsonResponse {+  timestamp :: Text+  , versions :: [Version]+  } deriving (Show, Generic, FromJSON, ToJSON)++findChromeInEnvironment :: IO String+findChromeInEnvironment =+  flip fix candidates $ \loop cs -> case cs of+    [] -> pure "google-chrome" -- Give up+    (candidate:rest) -> findExecutable candidate >>= \case+      Nothing -> loop rest+      Just _ -> pure candidate+  where+    candidates = [+      "google-chrome"+      , "google-chrome-stable" -- May be found on NixOS+      ]++detectChromeVersion :: FilePath -> IO (Either T.Text ChromeVersion)+detectChromeVersion chromeToUse = leftOnException $ runExceptT $ do+  (exitCode, stdout, stderr) <- liftIO $ readCreateProcessWithExitCode (shell (chromeToUse <> " --version | grep -Eo \"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\"")) ""++  rawString <- case exitCode of+    ExitFailure _ -> throwE [i|Couldn't parse google-chrome version. Stdout: '#{stdout}'. Stderr: '#{stderr}'|]+    ExitSuccess -> return $ T.strip $ T.pack stdout++  case T.splitOn "." rawString of+    [tReadMay -> Just w, tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ ChromeVersion (w, x, y, z)+    _ -> throwE [i|Failed to parse google-chrome version from string: '#{rawString}'|]++getChromeDriverVersion :: FilePath -> IO (Either T.Text ChromeDriverVersion)+getChromeDriverVersion chromePath = runExceptT $ do+  chromeVersion <- ExceptT $ liftIO $ detectChromeVersion chromePath+  ExceptT $ getChromeDriverVersion' chromeVersion++getChromeDriverVersion' :: ChromeVersion -> IO (Either T.Text ChromeDriverVersion)+getChromeDriverVersion' (ChromeVersion (w, x, y, z))+  | w < 115 = do+      let url = [i|https://chromedriver.storage.googleapis.com/LATEST_RELEASE_#{w}.#{x}.#{y}|]+      handle (\(e :: HttpException) -> do+                return $ Left [i|Error when requesting '#{url}': '#{e}'|]+             )+             (do+                 result :: T.Text <- (TL.toStrict . TL.decodeUtf8) <$> simpleHttp url+                 case T.splitOn "." result of+                   [tReadMay -> Just w', tReadMay -> Just x', tReadMay -> Just y', tReadMay -> Just z'] -> return $ Right $ ChromeDriverVersionTuple (w', x', y', z')+                   _ -> return $ Left [i|Failed to parse chromedriver version from string: '#{result}'|]+             )+  | otherwise = do+      let url = [i|https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json|]+      handle (\(e :: HttpException) -> do+                return $ Left [i|Error when requesting '#{url}': '#{e}'|]+             )+             (do+                 result :: LB.ByteString <- simpleHttp url+                 case A.eitherDecode result of+                   Left err -> return $ Left [i|Failed to decode response from '#{url}': #{err}|]+                   Right (response :: JsonResponse) -> do+                     let matchingVersions = [v | v@(Version {..}) <- versions response+                                               , [i|#{w}.#{x}.#{y}.|] `T.isPrefixOf` version]++                     let exactMatch = headMay [v | v@(Version {..}) <- matchingVersions+                                                 , [i|#{w}.#{x}.#{y}.#{z}|] == version]++                     let versionList :: [Version]+                         versionList = (case exactMatch of Nothing -> id; Just v -> (v :)) matchingVersions++                     case headMay (mapMaybe extractSuitableChromeDriver versionList) of+                       Nothing -> return $ Left [i|Couldn't find chromedriver associated with any Chrome release|]+                       Just (tup, url') -> return $ Right $ ChromeDriverVersionExactUrl tup url'+             )++extractSuitableChromeDriver :: Version -> Maybe ((Int, Int, Int, Int), Text)+extractSuitableChromeDriver (Version { version=(parseTuple -> Just tup), downloads=(M.lookup "chromedriver" -> Just platforms) }) =+  case headMay [url | PlatformAndUrl {platform, url} <- platforms+                    , platform == desiredPlatform] of+    Nothing -> Nothing+    Just url -> Just (tup, url)+  where+    desiredPlatform = case (SI.os, SI.arch) of+      ("windows", "x86_64") -> "win64"+      ("windows", "i386") -> "win32"+      ("mingw32", "x86_64") -> "win64"+      ("mingw32", "i386") -> "win32"++      ("darwin", "x86_64") -> "mac-x64"+      ("darwin", "arm") -> "mac-arm64"++      ("linux", _) -> "linux64"+      ("freebsd", _) -> "linux64"+      ("netbsd", _) -> "linux64"+      ("openbsd", _) -> "linux64"++      _ -> "unknown"+extractSuitableChromeDriver _ = Nothing++parseTuple :: Text -> Maybe (Int, Int, Int, Int)+parseTuple (T.splitOn "." -> [tReadMay -> Just w, tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z]) = Just (w, x, y, z)+parseTuple _ = Nothing++getChromeDriverDownloadUrl :: ChromeDriverVersion -> Platform -> T.Text+getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) Linux = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_linux64.zip|]+getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) OSX = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_mac64.zip|]+getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) Windows = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_win32.zip|]+getChromeDriverDownloadUrl (ChromeDriverVersionExactUrl _ url) _ = url++-- * Util++tReadMay :: T.Text -> Maybe Int+tReadMay = readMay . T.unpack
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Chrome/Types.hs view
@@ -0,0 +1,41 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Types (+  ChromeToUse(..)+  , ChromeDriverToUse(..)+  , ChromeVersion(..)+  , ChromeDriverVersion(..)+  ) where++import Data.Text as T+import Test.Sandwich.Contexts.Nix++-- | How to obtain the chrome binary.+data ChromeToUse =+  -- | Search the PATH for the @google-chrome@ or @google-chrome-stable@ binary.+  UseChromeFromPath+  -- | Use the Chrome at the given path.+  | UseChromeAt FilePath+  -- | Get Chrome from Nixpkgs.+  | UseChromeFromNixpkgs NixContext+  deriving Show++-- | How to obtain the chromedriver binary.+data ChromeDriverToUse =+  DownloadChromeDriverFrom FilePath String+  -- ^ Download chromedriver from the given URL to the 'toolsRoot'.+  | DownloadChromeDriverVersion FilePath ChromeDriverVersion+  -- ^ Download the given chromedriver version to the 'toolsRoot'.+  | DownloadChromeDriverAutodetect FilePath FilePath+  -- ^ Autodetect chromedriver to use based on the Chrome version and download it to the 'toolsRoot'+  -- Pass the path to the Chrome binary, or else it will be found by looking for google-chrome on the PATH.+  | UseChromeDriverAt FilePath+  -- ^ Use the chromedriver at the given path.+  | UseChromeDriverFromNixpkgs NixContext+  -- ^ Use the chromedriver in the given Nixpkgs derivation.+  deriving Show++newtype ChromeVersion = ChromeVersion (Int, Int, Int, Int) deriving Show+data ChromeDriverVersion =+  ChromeDriverVersionTuple (Int, Int, Int, Int)+  | ChromeDriverVersionExactUrl (Int, Int, Int, Int) Text+  deriving Show
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Common.hs view
@@ -0,0 +1,58 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Common where++import Control.Exception+import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Data.String.Interpolate+import qualified Data.Text as T+import System.Directory+import System.Exit+import System.FilePath+import System.Process+import Test.Sandwich.Expectations+import Test.Sandwich.Logging+import Test.Sandwich.WebDriver.Internal.Util+import UnliftIO.Temporary+++downloadAndUnzipToPath :: (MonadUnliftIO m, MonadLogger m) => T.Text -> FilePath -> m (Either T.Text ())+downloadAndUnzipToPath downloadPath localPath = leftOnException' $ do+  info [i|Downloading #{downloadPath} to #{localPath}|]+  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)+  withSystemTempDirectory "sandwich-webdriver-tool-download" $ \dir -> do+    curlDownloadToPath (T.unpack downloadPath) (dir </> "temp.zip")++    createProcessWithLogging ((proc "unzip" ["temp.zip", "-d", "unzipped"]) { cwd = Just dir })+      >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)+    let unzipped = dir </> "unzipped"++    executables <- (filter (/= "") . T.splitOn "\n" . T.pack) <$> readCreateProcessWithLogging (proc "find" [unzipped, "-executable", "-type", "f"]) ""+    case executables of+      [] -> liftIO $ throwIO $ userError [i|No executable found in file downloaded from #{downloadPath}|]+      [x] -> do+        liftIO $ copyFile (T.unpack x) localPath+        createProcessWithLogging (shell [i|chmod u+x #{localPath}|])+          >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)+      xs -> liftIO $ throwIO $ userError [i|Found multiple executable found in file downloaded from #{downloadPath}: #{xs}|]++downloadAndUntarballToPath :: (MonadUnliftIO m, MonadLogger m) => T.Text -> FilePath -> m (Either T.Text ())+downloadAndUntarballToPath downloadPath localPath = leftOnException' $ do+  info [i|Downloading #{downloadPath} to #{localPath}|]+  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)+  createProcessWithLogging (shell [i|wget -qO- #{downloadPath} | tar xvz  -C #{takeDirectory localPath}|])+    >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)+  createProcessWithLogging (shell [i|chmod u+x #{localPath}|])+    >>= liftIO . waitForProcess >>= (`shouldBe` ExitSuccess)++curlDownloadToPath :: (MonadUnliftIO m, MonadLogger m) => String -> FilePath -> m ()+curlDownloadToPath downloadPath localPath = do+  info [i|Downloading #{downloadPath} to #{localPath}|]+  liftIO $ createDirectoryIfMissing True (takeDirectory localPath)+  p <- createProcessWithLogging (proc "curl" [downloadPath, "-o", localPath, "-s"])+  liftIO (waitForProcess p) >>= (`shouldBe` ExitSuccess)++unlessM :: Monad m => m Bool -> m () -> m ()+unlessM b s = b >>= (\t -> unless t s)
− src/Test/Sandwich/WebDriver/Internal/Binaries/DetectChrome.hs
@@ -1,157 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}--module Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome (-  detectChromeVersion-  , getChromeDriverVersion-  , getChromeDriverDownloadUrl-  ) where--import Control.Exception-import Control.Monad.IO.Class-import Control.Monad.Trans.Except-import Data.Aeson as A-import qualified Data.ByteString.Lazy as LB-import Data.Function-import Data.Map as M hiding (mapMaybe)-import Data.Maybe (mapMaybe)-import Data.String.Interpolate-import Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Encoding as TL-import GHC.Generics-import Network.HTTP.Client-import Network.HTTP.Conduit (simpleHttp)-import Safe-import System.Directory (findExecutable)-import System.Exit-import qualified System.Info as SI-import System.Process-import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform-import Test.Sandwich.WebDriver.Internal.Types-import Test.Sandwich.WebDriver.Internal.Util---data PlatformAndUrl = PlatformAndUrl {-  platform :: Text-  , url :: Text-  } deriving (Show, Generic, FromJSON, ToJSON)--data Version = Version {-  version :: Text-  , revision :: Text-  , downloads :: Map Text [PlatformAndUrl]-  } deriving (Show, Generic, FromJSON, ToJSON)--data JsonResponse = JsonResponse {-  timestamp :: Text-  , versions :: [Version]-  } deriving (Show, Generic, FromJSON, ToJSON)--findChromeInEnvironment :: IO String-findChromeInEnvironment =-  flip fix candidates $ \loop cs -> case cs of-    [] -> pure "google-chrome" -- Give up-    (candidate:rest) -> findExecutable candidate >>= \case-      Nothing -> loop rest-      Just _ -> pure candidate-  where-    candidates = [-      "google-chrome"-      , "google-chrome-stable" -- May be found on NixOS-      ]--detectChromeVersion :: Maybe FilePath -> IO (Either T.Text ChromeVersion)-detectChromeVersion maybeChromePath = leftOnException $ runExceptT $ do-  chromeToUse <- liftIO $ maybe findChromeInEnvironment pure maybeChromePath--  (exitCode, stdout, stderr) <- liftIO $ readCreateProcessWithExitCode (shell (chromeToUse <> " --version | grep -Eo \"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+\"")) ""--  rawString <- case exitCode of-    ExitFailure _ -> throwE [i|Couldn't parse google-chrome version. Stdout: '#{stdout}'. Stderr: '#{stderr}'|]-    ExitSuccess -> return $ T.strip $ T.pack stdout--  case T.splitOn "." rawString of-    [tReadMay -> Just w, tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ ChromeVersion (w, x, y, z)-    _ -> throwE [i|Failed to parse google-chrome version from string: '#{rawString}'|]--getChromeDriverVersion :: Maybe FilePath -> IO (Either T.Text ChromeDriverVersion)-getChromeDriverVersion maybeChromePath = runExceptT $ do-  chromeVersion <- ExceptT $ liftIO $ detectChromeVersion maybeChromePath-  ExceptT $ getChromeDriverVersion' chromeVersion--getChromeDriverVersion' :: ChromeVersion -> IO (Either T.Text ChromeDriverVersion)-getChromeDriverVersion' (ChromeVersion (w, x, y, z))-  | w < 115 = do-      let url = [i|https://chromedriver.storage.googleapis.com/LATEST_RELEASE_#{w}.#{x}.#{y}|]-      handle (\(e :: HttpException) -> do-                return $ Left [i|Error when requesting '#{url}': '#{e}'|]-             )-             (do-                 result :: T.Text <- (TL.toStrict . TL.decodeUtf8) <$> simpleHttp url-                 case T.splitOn "." result of-                   [tReadMay -> Just w, tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ Right $ ChromeDriverVersionTuple (w, x, y, z)-                   _ -> return $ Left [i|Failed to parse chromedriver version from string: '#{result}'|]-             )-  | otherwise = do-      let url = [i|https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json|]-      handle (\(e :: HttpException) -> do-                return $ Left [i|Error when requesting '#{url}': '#{e}'|]-             )-             (do-                 result :: LB.ByteString <- simpleHttp url-                 case A.eitherDecode result of-                   Left err -> return $ Left [i|Failed to decode response from '#{url}': #{err}|]-                   Right (response :: JsonResponse) -> do-                     let matchingVersions = [v | v@(Version {..}) <- versions response-                                               , [i|#{w}.#{x}.#{y}.|] `T.isPrefixOf` version]--                     let exactMatch = headMay [x | x@(Version {..}) <- matchingVersions-                                               , [i|#{w}.#{x}.#{y}.#{z}|] == version]--                     let versionList :: [Version]-                         versionList = (case exactMatch of Nothing -> id; Just x -> (x :)) matchingVersions--                     case headMay (mapMaybe extractSuitableChromeDriver versionList) of-                       Nothing -> return $ Left [i|Couldn't find chromedriver associated with any Chrome release|]-                       Just (tup, url) -> return $ Right $ ChromeDriverVersionExactUrl tup url-             )--extractSuitableChromeDriver :: Version -> Maybe ((Int, Int, Int, Int), Text)-extractSuitableChromeDriver (Version { version=(parseTuple -> Just tup), downloads=(M.lookup "chromedriver" -> Just platforms) }) =-  case headMay [url | PlatformAndUrl {platform, url} <- platforms-                    , platform == desiredPlatform] of-    Nothing -> Nothing-    Just url -> Just (tup, url)-  where-    desiredPlatform = case (SI.os, SI.arch) of-      ("windows", "x86_64") -> "win64"-      ("windows", "i386") -> "win32"-      ("mingw32", "x86_64") -> "win64"-      ("mingw32", "i386") -> "win32"--      ("darwin", "x86_64") -> "mac-x64"-      ("darwin", "arm") -> "mac-arm64"--      ("linux", _) -> "linux64"-      ("freebsd", _) -> "linux64"-      ("netbsd", _) -> "linux64"-      ("openbsd", _) -> "linux64"--      _ -> "unknown"-extractSuitableChromeDriver _ = Nothing--parseTuple :: Text -> Maybe (Int, Int, Int, Int)-parseTuple (T.splitOn "." -> [tReadMay -> Just w, tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z]) = Just (w, x, y, z)-parseTuple _ = Nothing--getChromeDriverDownloadUrl :: ChromeDriverVersion -> Platform -> T.Text-getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) Linux = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_linux64.zip|]-getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) OSX = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_mac64.zip|]-getChromeDriverDownloadUrl (ChromeDriverVersionTuple (w, x, y, z)) Windows = [i|https://chromedriver.storage.googleapis.com/#{w}.#{x}.#{y}.#{z}/chromedriver_win32.zip|]-getChromeDriverDownloadUrl (ChromeDriverVersionExactUrl _ url) _ = url---- * Util--tReadMay = readMay . T.unpack
− src/Test/Sandwich/WebDriver/Internal/Binaries/DetectFirefox.hs
@@ -1,77 +0,0 @@-{-# LANGUAGE CPP #-}--module Test.Sandwich.WebDriver.Internal.Binaries.DetectFirefox (-  detectFirefoxVersion-  , getGeckoDriverVersion-  , getGeckoDriverDownloadUrl-  ) where--import Control.Exception-import Control.Monad.IO.Class-import Control.Monad.Trans.Except-import qualified Data.Aeson as A-import Data.Maybe-import Data.String.Interpolate-import qualified Data.Text as T-import Network.HTTP.Client-import Network.HTTP.Client.TLS-import Safe-import System.Exit-import System.Process-import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform-import Test.Sandwich.WebDriver.Internal.Types-import Test.Sandwich.WebDriver.Internal.Util--#if MIN_VERSION_aeson(2,0,0)-import qualified Data.Aeson.KeyMap          as HM-#else-import qualified Data.HashMap.Strict        as HM-#endif---detectFirefoxVersion :: Maybe FilePath -> IO (Either T.Text FirefoxVersion)-detectFirefoxVersion maybeFirefoxPath = leftOnException $ runExceptT $ do-  let firefoxToUse = fromMaybe "firefox" maybeFirefoxPath-  (exitCode, stdout, stderr) <- liftIO $ readCreateProcessWithExitCode (shell (firefoxToUse <> " --version | grep -Eo \"[0-9]+\\.[0-9]+(\\.[0-9]+)?\"")) ""--  rawString <- case exitCode of-                 ExitFailure _ -> throwE [i|Couldn't parse firefox version. Stdout: '#{stdout}'. Stderr: '#{stderr}'|]-                 ExitSuccess -> return $ T.strip $ T.pack stdout--  case T.splitOn "." rawString of-    [tReadMay -> Just x, tReadMay -> Just y] -> return $ FirefoxVersion (x, y, 0)-    [tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ FirefoxVersion (x, y, z)-    _ -> throwE [i|Failed to parse firefox version from string: '#{rawString}'|]---getGeckoDriverVersion :: Maybe FilePath -> IO (Either T.Text GeckoDriverVersion)-getGeckoDriverVersion _maybeFirefoxPath = runExceptT $ do-  -- firefoxVersion <- ExceptT $ liftIO $ detectFirefoxVersion maybeFirefoxPath--  -- Just get the latest release on GitHub-  let url = [i|https://api.github.com/repos/mozilla/geckodriver/releases/latest|]-  req <- parseRequest url-  manager <- liftIO newTlsManager-  ExceptT $-    handle (\(e :: HttpException) -> return $ Left [i|Error when requesting '#{url}': '#{e}'|])-           (do-               result <- httpLbs (req { requestHeaders = ("User-Agent", "foo") : (requestHeaders req) }) manager-               case A.eitherDecode $ responseBody result of-                 Right (A.Object (HM.lookup "tag_name" -> Just (A.String tag))) -> do-                   let parts = T.splitOn "." $ T.drop 1 tag-                   case parts of-                     [tReadMay -> Just x, tReadMay -> Just y] -> return $ Right $ GeckoDriverVersion (x, y, 0)-                     [tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ Right $ GeckoDriverVersion (x, y, z)-                     _ -> return $ Left [i|Unexpected geckodriver release tag: '#{tag}'|]-                 val -> return $ Left [i|Failed to decode GitHub releases: '#{val}'|]-           )---getGeckoDriverDownloadUrl :: GeckoDriverVersion -> Platform -> T.Text-getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) Linux = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-linux64.tar.gz|]-getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) OSX = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-macos.tar.gz|]-getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) Windows = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-win32.tar.gz|]---- * Util--tReadMay = readMay . T.unpack
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Ffmpeg.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg (+  obtainFfmpeg++  -- * Types+  , FfmpegToUse(..)+  ) where++import Control.Monad.Catch (MonadMask)+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Data.String.Interpolate+import qualified Data.Text as T+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg.Types+import UnliftIO.Directory+++-- | Manually obtain an ffmpeg binary, according to the 'FfmpegToUse' policy.+obtainFfmpeg :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLoggerIO m, MonadMask m+  ) => FfmpegToUse -> m (Either T.Text FilePath)+obtainFfmpeg UseFfmpegFromPath = findExecutable "ffmpeg" >>= \case+  Nothing -> return $ Left [i|Couldn't find "ffmpeg" on the PATH.|]+  Just p -> return $ Right p+obtainFfmpeg (UseFfmpegAt path) = doesFileExist path >>= \case+  False -> return $ Left [i|Path '#{path}' didn't exist|]+  True -> return $ Right path+obtainFfmpeg (UseFfmpegFromNixpkgs nixContext) =+  Right <$> getBinaryViaNixDerivation' @"ffmpeg" nixContext ffmpegDerivation+++ffmpegDerivation :: T.Text+ffmpegDerivation = [i|+{ ffmpeg+}:++ffmpeg.override { withXcb = true; }+|]
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Ffmpeg/Types.hs view
@@ -0,0 +1,16 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg.Types (+  FfmpegToUse(..)+  ) where++import Test.Sandwich.Contexts.Nix++-- | How to obtain the @ffmpeg@ binary.+data FfmpegToUse =+  -- | Search the PATH for the @ffmpeg@ binary.+  UseFfmpegFromPath+  -- | Use the @ffmpeg@ at the given path.+  | UseFfmpegAt FilePath+  -- | Get @ffmpeg@ from Nixpkgs.+  | UseFfmpegFromNixpkgs NixContext+  deriving Show
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Firefox.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Firefox (+  obtainFirefox+  , obtainGeckoDriver++  , FirefoxToUse(..)+  , GeckoDriverToUse(..)+  , GeckoDriverVersion(..)+  ) where++import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Control.Monad.Trans.Except+import Data.String.Interpolate+import qualified Data.Text as T+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.WebDriver.Internal.Binaries.Common+import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform+import Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Detect+import Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Types+import UnliftIO.Directory+++-- | Manually obtain a firefox binary, according to the 'FirefoxToUse' policy,+obtainFirefox :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLogger m+  ) => FirefoxToUse -> m (Either T.Text FilePath)+obtainFirefox UseFirefoxFromPath = do+  findExecutable "firefox" >>= \case+    Just p -> return $ Right p+    Nothing -> expectationFailure [i|Couldn't find "firefox" on the PATH|]+obtainFirefox (UseFirefoxAt p) = doesFileExist p >>= \case+  False -> return $ Left [i|Path '#{p}' didn't exist|]+  True -> return $ Right p+obtainFirefox (UseFirefoxFromNixpkgs nixContext) =+  Right <$> getBinaryViaNixPackage' @"firefox" nixContext "firefox"++-- | Manually obtain a @geckodriver@ binary, according to the 'GeckoDriverToUse' policy,+-- storing it under the provided 'FilePath' if necessary and returning the exact path.+obtainGeckoDriver :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLogger m+  )+  -- | How to obtain @geckodriver@+  => GeckoDriverToUse+  -> m (Either T.Text FilePath)+obtainGeckoDriver (DownloadGeckoDriverFrom toolsDir url) = do+  let path = [i|#{toolsDir}/#{geckoDriverExecutable}|]+  unlessM (liftIO $ doesFileExist path) $+    curlDownloadToPath url path+  return $ Right path+obtainGeckoDriver (DownloadGeckoDriverVersion toolsDir geckoDriverVersion) = runExceptT $ do+  let path = getGeckoDriverPath toolsDir geckoDriverVersion+  liftIO (doesFileExist path) >>= \case+    True -> return path+    False -> do+      let downloadPath = getGeckoDriverDownloadUrl geckoDriverVersion detectPlatform+      ExceptT $ downloadAndUntarballToPath downloadPath path+      return path+obtainGeckoDriver (DownloadGeckoDriverAutodetect toolsDir) = runExceptT $ do+  version <- ExceptT $ liftIO $ getGeckoDriverVersion Nothing+  ExceptT $ obtainGeckoDriver (DownloadGeckoDriverVersion toolsDir version)+obtainGeckoDriver (UseGeckoDriverAt path) = liftIO (doesFileExist path) >>= \case+  False -> return $ Left [i|Path '#{path}' didn't exist|]+  True -> return $ Right path+obtainGeckoDriver (UseGeckoDriverFromNixpkgs nixContext) = Right <$> getBinaryViaNixPackage' @"geckodriver" nixContext "geckodriver"+++getGeckoDriverPath :: FilePath -> GeckoDriverVersion -> FilePath+getGeckoDriverPath toolsDir (GeckoDriverVersion (x, y, z)) = [i|#{toolsDir}/geckodrivers/#{x}.#{y}.#{z}/#{geckoDriverExecutable}|]++geckoDriverExecutable :: T.Text+geckoDriverExecutable = case detectPlatform of+  Windows -> "geckodriver.exe"+  _ -> "geckodriver"
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Firefox/Detect.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE CPP #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Detect (+  detectFirefoxVersion+  , getGeckoDriverVersion+  , getGeckoDriverDownloadUrl+  ) where++import Control.Exception+import Control.Monad.IO.Class+import Control.Monad.Trans.Except+import qualified Data.Aeson as A+import Data.Maybe+import Data.String.Interpolate+import qualified Data.Text as T+import Network.HTTP.Client+import Network.HTTP.Client.TLS+import Safe+import System.Exit+import System.Process+import Test.Sandwich.WebDriver.Internal.Binaries.DetectPlatform+import Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Types+import Test.Sandwich.WebDriver.Internal.Util++#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.KeyMap          as HM+#else+import qualified Data.HashMap.Strict        as HM+#endif+++detectFirefoxVersion :: Maybe FilePath -> IO (Either T.Text FirefoxVersion)+detectFirefoxVersion maybeFirefoxPath = leftOnException $ runExceptT $ do+  let firefoxToUse = fromMaybe "firefox" maybeFirefoxPath+  (exitCode, stdout, stderr) <- liftIO $ readCreateProcessWithExitCode (shell (firefoxToUse <> " --version | grep -Eo \"[0-9]+\\.[0-9]+(\\.[0-9]+)?\"")) ""++  rawString <- case exitCode of+                 ExitFailure _ -> throwE [i|Couldn't parse firefox version. Stdout: '#{stdout}'. Stderr: '#{stderr}'|]+                 ExitSuccess -> return $ T.strip $ T.pack stdout++  case T.splitOn "." rawString of+    [tReadMay -> Just x, tReadMay -> Just y] -> return $ FirefoxVersion (x, y, 0)+    [tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ FirefoxVersion (x, y, z)+    _ -> throwE [i|Failed to parse firefox version from string: '#{rawString}'|]+++getGeckoDriverVersion :: Maybe FilePath -> IO (Either T.Text GeckoDriverVersion)+getGeckoDriverVersion _maybeFirefoxPath = runExceptT $ do+  -- firefoxVersion <- ExceptT $ liftIO $ detectFirefoxVersion maybeFirefoxPath++  -- Just get the latest release on GitHub+  let url = [i|https://api.github.com/repos/mozilla/geckodriver/releases/latest|]+  req <- parseRequest url+  manager <- liftIO newTlsManager+  ExceptT $+    handle (\(e :: HttpException) -> return $ Left [i|Error when requesting '#{url}': '#{e}'|])+           (do+               result <- httpLbs (req { requestHeaders = ("User-Agent", "foo") : (requestHeaders req) }) manager+               case A.eitherDecode $ responseBody result of+                 Right (A.Object (HM.lookup "tag_name" -> Just (A.String tag))) -> do+                   let parts = T.splitOn "." $ T.drop 1 tag+                   case parts of+                     [tReadMay -> Just x, tReadMay -> Just y] -> return $ Right $ GeckoDriverVersion (x, y, 0)+                     [tReadMay -> Just x, tReadMay -> Just y, tReadMay -> Just z] -> return $ Right $ GeckoDriverVersion (x, y, z)+                     _ -> return $ Left [i|Unexpected geckodriver release tag: '#{tag}'|]+                 val -> return $ Left [i|Failed to decode GitHub releases: '#{val}'|]+           )+++getGeckoDriverDownloadUrl :: GeckoDriverVersion -> Platform -> T.Text+getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) Linux = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-linux64.tar.gz|]+getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) OSX = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-macos.tar.gz|]+getGeckoDriverDownloadUrl (GeckoDriverVersion (x, y, z)) Windows = [i|https://github.com/mozilla/geckodriver/releases/download/v#{x}.#{y}.#{z}/geckodriver-v#{x}.#{y}.#{z}-win32.tar.gz|]++-- * Util++tReadMay :: T.Text -> Maybe Int+tReadMay = readMay . T.unpack
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Firefox/Types.hs view
@@ -0,0 +1,32 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Firefox.Types where++import Test.Sandwich.Contexts.Nix+++-- | How to obtain the @firefox@ binary.+data FirefoxToUse =+  -- | Search the PATH for the @firefox@ binary.+  UseFirefoxFromPath+  -- | Use the Firefox at the given path.+  | UseFirefoxAt FilePath+  -- | Get Firefox from Nixpkgs.+  | UseFirefoxFromNixpkgs NixContext+  deriving Show++-- | How to obtain the @geckodriver@ binary.+data GeckoDriverToUse =+  DownloadGeckoDriverFrom FilePath String+  -- ^ Download @geckodriver@ from the given URL to the 'toolsRoot'.+  | DownloadGeckoDriverVersion FilePath GeckoDriverVersion+  -- ^ Download the given @geckodriver@ version to the 'toolsRoot'.+  | DownloadGeckoDriverAutodetect FilePath+  -- ^ Autodetect @geckodriver@ to use based on the Firefox version and download it to the 'toolsRoot'.+  | UseGeckoDriverAt FilePath+  -- ^ Use the @geckodriver@ at the given path.+  | UseGeckoDriverFromNixpkgs NixContext+  -- ^ Use the @geckodriver@ in the given Nixpkgs derivation.+  deriving Show++newtype FirefoxVersion = FirefoxVersion (Int, Int, Int) deriving Show+newtype GeckoDriverVersion = GeckoDriverVersion (Int, Int, Int) deriving Show
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Selenium.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE ConstraintKinds #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Selenium (+  obtainSelenium+  , downloadSeleniumIfNecessary+  , SeleniumToUse(..)+  ) where++import Control.Monad+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import qualified Data.List as L+import Data.String.Interpolate+import qualified Data.Text as T+import GHC.Stack+import System.Directory+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.Contexts.Nix+import Test.Sandwich.WebDriver.Internal.Binaries.Common+import Test.Sandwich.WebDriver.Internal.Binaries.Selenium.Types+import Test.Sandwich.WebDriver.Internal.Util+++type Constraints m = (+  HasCallStack+  , MonadLogger m+  , MonadUnliftIO m+  )++-- * Obtaining binaries++defaultSeleniumJarUrl :: String+defaultSeleniumJarUrl = "https://selenium-release.storage.googleapis.com/3.141/selenium-server-standalone-3.141.59.jar"++-- | Manually obtain a Selenium server JAR file, according to the 'SeleniumToUse' policy,+-- storing it under the provided 'FilePath' if necessary and returning the exact path.+obtainSelenium :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLogger m+  )+  -- | How to obtain Selenium+  => SeleniumToUse+  -> m FilePath+obtainSelenium (DownloadSeleniumFrom toolsDir url) = do+  let path = [i|#{toolsDir}/selenium-server-standalone.jar|]+  unlessM (liftIO $ doesFileExist path) $+    curlDownloadToPath url path+  return path+obtainSelenium (DownloadSeleniumDefault toolsDir) = do+  let path = [i|#{toolsDir}/selenium-server-standalone-3.141.59.jar|]+  unlessM (liftIO $ doesFileExist path) $+    curlDownloadToPath defaultSeleniumJarUrl path+  return path+obtainSelenium (UseSeleniumAt path) = liftIO (doesFileExist path) >>= \case+  False -> expectationFailure [i|Path '#{path}' didn't exist|]+  True -> return path+obtainSelenium (UseSeleniumFromNixpkgs nixContext) = do+  buildNixSymlinkJoin' nixContext ["selenium-server-standalone"] >>=+    liftIO . findFirstFile (return . (".jar" `L.isSuffixOf`))+++-- * Lower level helpers+++downloadSeleniumIfNecessary :: Constraints m => FilePath -> m (Either T.Text FilePath)+downloadSeleniumIfNecessary toolsDir = leftOnException' $ do+  let seleniumPath = [i|#{toolsDir}/selenium-server.jar|]+  liftIO (doesFileExist seleniumPath) >>= flip unless (downloadSelenium seleniumPath)+  return seleniumPath+  where+    downloadSelenium :: Constraints m => FilePath -> m ()+    downloadSelenium seleniumPath = void $ do+      info [i|Downloading selenium-server.jar to #{seleniumPath}|]+      curlDownloadToPath defaultSeleniumJarUrl seleniumPath
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Selenium/Types.hs view
@@ -0,0 +1,17 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Selenium.Types where++import Test.Sandwich.Contexts.Nix+++-- | How to obtain the Selenium server JAR file.+data SeleniumToUse =+  DownloadSeleniumFrom FilePath String+  -- ^ Download selenium from the given URL to the 'toolsRoot'+  | DownloadSeleniumDefault FilePath+  -- ^ Download selenium from a default location to the 'toolsRoot'+  | UseSeleniumAt FilePath+  -- ^ Use the JAR file at the given path+  | UseSeleniumFromNixpkgs NixContext+  -- ^ Use the Selenium in the given Nixpkgs derivation+  deriving Show
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Xvfb.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}++module Test.Sandwich.WebDriver.Internal.Binaries.Xvfb (+  obtainXvfb++  -- * Types+  , XvfbDependenciesSpec(..)+  , XvfbToUse(..)+  , FluxboxToUse(..)+  ) where++import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Data.String.Interpolate+import qualified Data.Text as T+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.WebDriver.Internal.Binaries.Xvfb.Types+import UnliftIO.Directory+++-- | Manually obtain an Xvfb binary, according to the 'XvfbToUse' policy.+obtainXvfb :: (+  MonadReader context m, HasBaseContext context+  , MonadUnliftIO m, MonadLoggerIO m+  ) => XvfbToUse -> m (Either T.Text FilePath)+obtainXvfb UseXvfbFromPath = findExecutable "Xvfb" >>= \case+  Nothing -> return $ Left [i|Couldn't find "Xvfb" on the PATH.|]+  Just p -> return $ Right p+obtainXvfb (UseXvfbAt path) = doesFileExist path >>= \case+  False -> return $ Left [i|Path '#{path}' didn't exist|]+  True -> return $ Right path+obtainXvfb (UseXvfbFromNixpkgs nixContext) =+  -- Note: on master *after* release-24.05, there seems to be xorg.xvfb+  Right <$> getBinaryViaNixPackage' @"Xvfb" nixContext "xorg.xorgserver"
+ src/Test/Sandwich/WebDriver/Internal/Binaries/Xvfb/Types.hs view
@@ -0,0 +1,33 @@++module Test.Sandwich.WebDriver.Internal.Binaries.Xvfb.Types (+  XvfbDependenciesSpec(..)+  , XvfbToUse(..)+  , FluxboxToUse(..)+  ) where++import Test.Sandwich.Contexts.Nix++data XvfbDependenciesSpec = XvfbDependenciesSpec {+  xvfbDependenciesSpecXvfb :: XvfbToUse+  , xvfbDependenciesSpecFluxbox :: Maybe FluxboxToUse+  }++-- | How to obtain the @Xvfb@ binary.+data XvfbToUse =+  -- | Search the PATH for the @Xvfb@ binary.+  UseXvfbFromPath+  -- | Use the @Xvfb@ at the given path.+  | UseXvfbAt FilePath+  -- | Get @Xvfb@ from Nixpkgs.+  | UseXvfbFromNixpkgs NixContext+  deriving Show++-- | How to obtain the @fluxbox@ binary.+data FluxboxToUse =+  -- | Search the PATH for the @fluxbox@ binary.+  UseFluxboxFromPath+  -- | Use the @fluxbox@ at the given path.+  | UseFluxboxAt FilePath+  -- | Get @fluxbox@ from Nixpkgs.+  | UseFluxboxFromNixpkgs NixContext+  deriving Show
src/Test/Sandwich/WebDriver/Internal/Capabilities.hs view
@@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedLists #-}--- |  module Test.Sandwich.WebDriver.Internal.Capabilities (   -- * Chrome@@ -20,12 +19,13 @@ import qualified Test.WebDriver.Firefox.Profile as FF  loggingPrefs :: A.Value-loggingPrefs = A.object [("browser", "ALL")-                        , ("client", "WARNING")-                        , ("driver", "WARNING")-                        , ("performance", "ALL")-                        , ("server", "WARNING")-                        ]+loggingPrefs = A.object [+  ("browser", "ALL")+  , ("client", "WARNING")+  , ("driver", "WARNING")+  , ("performance", "ALL")+  , ("server", "WARNING")+  ]  -- * Chrome 
+ src/Test/Sandwich/WebDriver/Internal/Capabilities/Extra.hs view
@@ -0,0 +1,130 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE OverloadedLists #-}++module Test.Sandwich.WebDriver.Internal.Capabilities.Extra (+  configureHeadlessCapabilities+  , configureDownloadCapabilities+  ) where++import Control.Monad.Catch (MonadMask)+import Control.Monad.IO.Class+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import qualified Data.Aeson as A+import Data.Function+import qualified Data.List as L+import Data.Maybe+import Data.String.Interpolate+import qualified Data.Text as T+import qualified Data.Vector as V+import GHC.Stack+import Lens.Micro+import Lens.Micro.Aeson+import Test.Sandwich+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Detect (detectChromeVersion)+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome.Types (ChromeVersion(..))+import Test.Sandwich.WebDriver.Internal.Types+import qualified Test.WebDriver as W+import qualified Test.WebDriver.Firefox.Profile as FF+++#if MIN_VERSION_aeson(2,0,0)+import qualified Data.Aeson.Key             as A+import qualified Data.Aeson.KeyMap          as HM+fromText :: T.Text -> A.Key+fromText = A.fromText+#else+import qualified Data.HashMap.Strict        as HM+fromText :: T.Text -> T.Text+fromText = id+#endif+++type Constraints m = (HasCallStack, MonadLogger m, MonadUnliftIO m, MonadMask m)++-- | Add headless configuration to the Chrome browser+configureHeadlessCapabilities :: (Constraints m) => WdOptions -> RunMode -> W.Capabilities -> m W.Capabilities+configureHeadlessCapabilities _wdOptions (RunHeadless (HeadlessConfig {..})) caps@(W.Capabilities {W.browser=browser@(W.Chrome {..})}) = do+  chromeBinaryPath <- case chromeBinary of+    Nothing -> expectationFailure [i|Chrome capabilities didn't define chromeBinary in configureHeadlessCapabilities|]+    Just x -> pure x++  headlessArg <- liftIO (detectChromeVersion chromeBinaryPath) >>= \case+    Left err -> do+      warn [i|Couldn't determine chrome version when configuring headless capabilities (err: #{err}); passing --headless|]+      return "--headless"+    Right (ChromeVersion (major, _, _, _))+      -- See https://www.selenium.dev/blog/2023/headless-is-going-away/+      | major >= 110 -> return "--headless=new"+      | otherwise -> return "--headless"++  let browser' = browser { W.chromeOptions = headlessArg:resolution:chromeOptions }++  return (caps { W.browser = browser' })++  where+    resolution = [i|--window-size=#{w},#{h}|]+    (w, h) = fromMaybe (1920, 1080) headlessResolution++-- | Add headless configuration to the Firefox capabilities+configureHeadlessCapabilities _ (RunHeadless (HeadlessConfig {})) caps@(W.Capabilities {W.browser=(W.Firefox {}), W.additionalCaps=ac}) = return (caps { W.additionalCaps = additionalCaps })+  where+    additionalCaps = case L.findIndex (\x -> fst x == "moz:firefoxOptions") ac of+      Nothing -> ("moz:firefoxOptions", A.object [("args", A.Array ["-headless"])]) : ac+      Just i' -> let ffOptions' = snd (ac !! i')+                                & ensureKeyExists "args" (A.Array [])+                                & ((key "args" . _Array) %~ addHeadlessArg) in+        L.nubBy (\x y -> fst x == fst y) (("moz:firefoxOptions", ffOptions') : ac)++    ensureKeyExists :: T.Text -> A.Value -> A.Value -> A.Value+    ensureKeyExists key' _ val@(A.Object (HM.lookup (fromText key') -> Just _)) = val+    ensureKeyExists key' defaultVal (A.Object m@(HM.lookup (fromText key') -> Nothing)) = A.Object (HM.insert (fromText key') defaultVal m)+    ensureKeyExists _ _ _ = error "Expected Object in ensureKeyExists"++    addHeadlessArg :: V.Vector A.Value -> V.Vector A.Value+    addHeadlessArg xs | (A.String "-headless") `V.elem` xs = xs+    addHeadlessArg xs = (A.String "-headless") `V.cons` xs++configureHeadlessCapabilities _ (RunHeadless {}) browser = error [i|Headless mode not yet supported for browser '#{browser}'|]+configureHeadlessCapabilities _ _ browser = return browser+++-- | Configure download capabilities to set the download directory and disable prompts+-- (since you can't test download prompts using Selenium)+configureDownloadCapabilities :: (+  MonadIO m+  ) => [Char] -> W.Capabilities -> m W.Capabilities+configureDownloadCapabilities downloadDir caps@(W.Capabilities {W.browser=browser@(W.Firefox {..})}) = do+  profile <- case ffProfile of+    Just x -> pure x+    Nothing -> liftIO $ FF.defaultProfile+      & FF.addPref "browser.download.folderList" (2 :: Int)+      & FF.addPref "browser.download.manager.showWhenStarting" False+      & FF.addPref "browser.download.dir" downloadDir+      & FF.addPref "browser.helperApps.neverAsk.saveToDisk" ("*" :: String)+      & FF.prepareProfile++  return (caps { W.browser = browser { W.ffProfile = Just profile } })+configureDownloadCapabilities downloadDir caps@(W.Capabilities {W.browser=browser@(W.Chrome {..})}) = return $ caps { W.browser=browser' }+  where+    browser' = browser { W.chromeExperimentalOptions = options }++    basePrefs :: A.Object+    basePrefs = case HM.lookup "prefs" chromeExperimentalOptions of+      Just (A.Object hm) -> hm+      Just x -> error [i|Expected chrome prefs to be object, got '#{x}'.|]+      Nothing -> mempty++    prefs :: A.Object+    prefs = basePrefs+          & foldl (.) id [HM.insert k v | (k, v) <- downloadPrefs]++    options = HM.insert "prefs" (A.Object prefs) chromeExperimentalOptions++    downloadPrefs = [("profile.default_content_setting_values.automatic_downloads", A.Number 1)+                    , ("profile.content_settings.exceptions.automatic_downloads.*.setting", A.Number 1)+                    , ("download.prompt_for_download", A.Bool False)+                    , ("download.directory_upgrade", A.Bool True)+                    , ("download.default_directory", A.String (T.pack downloadDir))]+configureDownloadCapabilities _ browser = return browser
+ src/Test/Sandwich/WebDriver/Internal/Dependencies.hs view
@@ -0,0 +1,176 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE QuantifiedConstraints #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}++module Test.Sandwich.WebDriver.Internal.Dependencies (+  WebDriverDependencies(..)+  , BrowserDependenciesSpec(..)++  , defaultWebDriverDependencies++  , BrowserDependencies(..)+  , browserDependencies+  , HasBrowserDependencies++  , getBrowserDependencies+  , introduceBrowserDependenciesViaNix+  , introduceBrowserDependenciesViaNix'+  , fillInCapabilitiesAndGetDriverArgs+  ) where++import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Data.String.Interpolate+import System.FilePath+import Test.Sandwich+import Test.Sandwich.Contexts.Files+import Test.Sandwich.Contexts.Nix+import Test.Sandwich.WebDriver.Internal.Binaries.Chrome+import Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg+import Test.Sandwich.WebDriver.Internal.Binaries.Firefox+import Test.Sandwich.WebDriver.Internal.Binaries.Selenium.Types+import Test.Sandwich.WebDriver.Internal.Binaries.Xvfb+import Test.Sandwich.WebDriver.Internal.Util+import qualified Test.WebDriver as W+++-- * All dependencies++-- | This type describes how we should obtain all the dependencies needed to launch a WebDriver session.+-- You can configure them individually.+data WebDriverDependencies = WebDriverDependencies {+  -- | Path to @java@ binary to use to start Selenium. If not provided, we'll search the PATH.+  webDriverDependencyJava :: Maybe FilePath+  -- | How to obtain a @selenium.jar@ file.+  , webDriverDependencySelenium :: SeleniumToUse+  -- | Browser/driver dependencies.+  , webDriverDependencyBrowser :: BrowserDependenciesSpec+  -- | How to obtain @Xvfb@ (used for the 'RunInXvfb' 'RunMode').+  , webDriverXvfb :: XvfbDependenciesSpec+  -- | How to obtain @ffmpeg@ (used for video recording).+  , webDriverFfmpeg :: FfmpegToUse+  }++-- | This type describes how to obain a browser + browser driver combination.+data BrowserDependenciesSpec = BrowserDependenciesSpecChrome {+  browserDependenciesSpecChromeChrome :: ChromeToUse+  , browserDependenciesSpecChromeChromeDriver :: ChromeDriverToUse+  }+  | BrowserDependenciesSpecFirefox {+      browserDependenciesSpecFirefoxFirefox :: FirefoxToUse+      , browserDependenciesSpecFirefoxGeckodriver :: GeckoDriverToUse+      }++-- | This configuration will+--+-- * Use @java@ from the PATH, failing if it isn't found.+-- * Download Selenium to @\/tmp\/tools@, reusing the one there if found.+-- * Use @firefox@ from the PATH as the browser.+-- * Download a compatible @geckodriver@ to @\/tmp\/tools@, reusing the one there if found.+-- * If applicable, it will also get @Xvfb@, @fluxbox@, and/or @ffmpeg@ from the PATH.+--+-- But, it's easy to customize this behavior. You can define your own 'WebDriverDependencies' and customize+-- how each of these dependencies are found.+defaultWebDriverDependencies = WebDriverDependencies {+  webDriverDependencyJava = Nothing+  , webDriverDependencySelenium = DownloadSeleniumDefault "/tmp/tools"+  , webDriverDependencyBrowser = BrowserDependenciesSpecFirefox UseFirefoxFromPath (DownloadGeckoDriverAutodetect "/tmp/tools")+  , webDriverXvfb = XvfbDependenciesSpec UseXvfbFromPath (Just UseFluxboxFromPath)+  , webDriverFfmpeg = UseFfmpegFromPath+  }++-- * Browser dependencies++data BrowserDependencies = BrowserDependenciesChrome {+  browserDependenciesChromeChrome :: FilePath+  , browserDependenciesChromeChromedriver :: FilePath+  }+  | BrowserDependenciesFirefox {+      browserDependenciesFirefoxFirefox :: FilePath+      , browserDependenciesFirefoxGeckodriver :: FilePath+      }+  deriving (Show)++browserDependencies :: Label "browserDependencies" BrowserDependencies+browserDependencies = Label++type HasBrowserDependencies context = HasLabel context "browserDependencies" BrowserDependencies++getBrowserDependencies :: (+  MonadUnliftIO m, MonadLogger m+  , MonadReader context m, HasBaseContext context+  ) => BrowserDependenciesSpec -> m BrowserDependencies+getBrowserDependencies BrowserDependenciesSpecChrome {..} = do+  chrome <- exceptionOnLeft $ obtainChrome browserDependenciesSpecChromeChrome+  chromeDriver <- exceptionOnLeft $ obtainChromeDriver browserDependenciesSpecChromeChromeDriver+  return $ BrowserDependenciesChrome chrome chromeDriver+getBrowserDependencies (BrowserDependenciesSpecFirefox {..}) = do+  firefox <- exceptionOnLeft $ obtainFirefox browserDependenciesSpecFirefoxFirefox+  geckoDriver <- exceptionOnLeft $ obtainGeckoDriver browserDependenciesSpecFirefoxGeckodriver+  return $ BrowserDependenciesFirefox firefox geckoDriver++-- | Introduce 'BrowserDependencies' via Nix, using the command line options.+-- This is useful to create the context for functions like 'allocateWebDriver'.+introduceBrowserDependenciesViaNix :: forall m context. (+  MonadUnliftIO m, HasBaseContext context, HasNixContext context, HasSomeCommandLineOptions context+  )+  -- | Child spec+  => SpecFree (LabelValue "browserDependencies" BrowserDependencies :> context) m ()+  -- | Parent spec+  -> SpecFree context m ()+introduceBrowserDependenciesViaNix = introduceBrowserDependenciesViaNix' (defaultNodeOptions { nodeOptionsVisibilityThreshold = 100 })++-- | Same as 'introduceBrowserDependenciesViaNix', but allows passing custom 'NodeOptions'.+introduceBrowserDependenciesViaNix' :: forall m context. (+  MonadUnliftIO m, HasBaseContext context, HasNixContext context, HasSomeCommandLineOptions context+  )+  => NodeOptions+  -- | Child spec+  -> SpecFree (LabelValue "browserDependencies" BrowserDependencies :> context) m ()+  -- | Parent spec+  -> SpecFree context m ()+introduceBrowserDependenciesViaNix' nodeOptions = introduce' nodeOptions "Introduce browser dependencies" browserDependencies alloc (const $ return ())+  where+    alloc = do+      SomeCommandLineOptions (CommandLineOptions {optWebdriverOptions=(CommandLineWebdriverOptions {..})}) <- getSomeCommandLineOptions++      let useChrome = BrowserDependenciesChrome <$> getBinaryViaNixPackage @"google-chrome-stable" "google-chrome"+                                                <*> getBinaryViaNixPackage @"chromedriver" "chromedriver"++      let useFirefox = BrowserDependenciesFirefox <$> getBinaryViaNixPackage @"firefox" "firefox"+                                                  <*> getBinaryViaNixPackage @"geckodriver" "geckodriver"++      deps <- case optFirefox of+        Just UseChrome -> useChrome+        Just UseFirefox -> useFirefox+        Nothing -> useChrome++      debug [i|Got browser dependencies: #{deps}|]++      return deps++fillInCapabilitiesAndGetDriverArgs webdriverRoot capabilities'' = getContext browserDependencies >>= \case+  BrowserDependenciesFirefox {..} -> do+    let args = [+          [i|-Dwebdriver.gecko.driver=#{browserDependenciesFirefoxGeckodriver}|]+          -- , [i|-Dwebdriver.gecko.logfile=#{webdriverRoot </> "geckodriver.log"}|]+          -- , [i|-Dwebdriver.gecko.verboseLogging=true|]+          ]+    let capabilities' = capabilities'' {+          W.browser = W.firefox { W.ffBinary = Just browserDependenciesFirefoxFirefox }+          }+    return (args, capabilities')+  BrowserDependenciesChrome {..} -> do+    let args = [+          [i|-Dwebdriver.chrome.driver=#{browserDependenciesChromeChromedriver}|]+          , [i|-Dwebdriver.chrome.logfile=#{webdriverRoot </> "chromedriver.log"}|]+          , [i|-Dwebdriver.chrome.verboseLogging=true|]+          ]+    let capabilities' = capabilities'' {+          W.browser = W.chrome { W.chromeBinary = Just browserDependenciesChromeChrome }+          }+    return (args, capabilities')
+ src/Test/Sandwich/WebDriver/Internal/OnDemand.hs view
@@ -0,0 +1,45 @@++module Test.Sandwich.WebDriver.Internal.OnDemand where++import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Data.String.Interpolate+import Data.Text as T+import Test.Sandwich+import Test.Sandwich.WebDriver.Internal.Types+import UnliftIO.Async+import UnliftIO.Exception+import UnliftIO.MVar+++getOnDemand :: forall m a. (+  MonadUnliftIO m, MonadLogger m+  ) => MVar (OnDemand a) -> m (Either Text a) -> m a+getOnDemand onDemandVar doObtain = do+  result <- modifyMVar onDemandVar $ \case+    OnDemandErrored msg -> expectationFailure (T.unpack msg)+    OnDemandNotStarted -> do+      asy <- async $ do+        let handler :: SomeException -> m a+            handler e = do+              modifyMVar_ onDemandVar (const $ return $ OnDemandErrored [i|Got exception: #{e}|])+              throwIO e++        handle handler $ do+          doObtain >>= \case+            Left err -> do+              modifyMVar_ onDemandVar (const $ return $ OnDemandErrored err)+              expectationFailure [i|Failed to obtain: #{err}|]++            Right x -> do+              modifyMVar_ onDemandVar (const $ return $ OnDemandReady x)+              return x++      return (OnDemandInProgress asy, Left asy)++    od@(OnDemandInProgress asy) -> pure (od, Left asy)+    od@(OnDemandReady x) -> pure (od, Right x)++  case result of+    Right x -> pure x+    Left asy -> wait asy
− src/Test/Sandwich/WebDriver/Internal/Ports.hs
@@ -1,78 +0,0 @@-{-# LANGUAGE RankNTypes, MultiWayIf, ScopedTypeVariables, LambdaCase #-}--module Test.Sandwich.WebDriver.Internal.Ports (-  findFreePortOrException-  ) where--import Control.Exception-import Control.Retry-import Data.Maybe-import Data.String.Interpolate-import qualified Data.Text as T-import Network.Socket-import System.Random (randomRIO)-import Test.Sandwich.WebDriver.Internal.Util--firstUserPort :: PortNumber-firstUserPort = 1024--highestPort :: PortNumber-highestPort = 65535---- |Find an unused port in a given range-findFreePortInRange' :: RetryPolicy -> IO PortNumber -> IO (Maybe PortNumber)-findFreePortInRange' policy getAcceptableCandidate = retrying policy (\_retryStatus result -> return $ isNothing result) (const findFreePortInRange'')-  where-    findFreePortInRange'' :: IO (Maybe PortNumber)-    findFreePortInRange'' = do-      candidate <- getAcceptableCandidate-      catch (tryOpenAndClosePort candidate >> return (Just candidate)) (\(_ :: SomeException) -> return Nothing)-      where-        tryOpenAndClosePort :: PortNumber -> IO PortNumber-        tryOpenAndClosePort port = do-          sock <- socket AF_INET Stream 0-          setSocketOption sock ReuseAddr 1-          let hostAddress = tupleToHostAddress (127, 0, 0, 1)-          bind sock (SockAddrInet port hostAddress)-          close sock-          return $ fromIntegral port--findFreePortInRange :: IO PortNumber -> IO (Maybe PortNumber)-findFreePortInRange = findFreePortInRange' (limitRetries 50)---- | Find an unused port in the ephemeral port range.--- See https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers--- This works without a timeout since there should always be a port in the somewhere;--- it might be advisable to wrap in a timeout anyway.-findFreePort :: IO (Maybe PortNumber)-findFreePort = findFreePortInRange getNonEphemeralCandidate--findFreePortOrException :: IO PortNumber-findFreePortOrException = findFreePort >>= \case-  Just port -> return port-  Nothing -> error "Couldn't find free port"---- * Util--getNonEphemeralCandidate :: IO PortNumber-getNonEphemeralCandidate = do-  (ephemeralStart, ephemeralEnd) <- getEphemeralPortRange >>= \case-    Left _ -> return (49152, 65535)-    Right range -> return range--  let numBelow = ephemeralStart - firstUserPort-  let numAbove = highestPort - ephemeralEnd--  u :: Double <- randomRIO (0, 1)--  let useLowerRange = u < ((fromIntegral numBelow) / (fromIntegral numBelow + fromIntegral numAbove))--  if | useLowerRange -> fromInteger <$> randomRIO (fromIntegral firstUserPort, fromIntegral ephemeralStart)-     | otherwise -> fromInteger <$> randomRIO (fromIntegral ephemeralEnd, fromIntegral highestPort)--getEphemeralPortRange :: IO (Either T.Text (PortNumber, PortNumber))-getEphemeralPortRange = leftOnException' $ do-  contents <- readFile "/proc/sys/net/ipv4/ip_local_port_range"-  case fmap read (words contents) of-    [p1, p2] -> return (p1, p2)-    _ -> error [i|Unexpected contents: '#{contents}'|]
src/Test/Sandwich/WebDriver/Internal/Screenshots.hs view
@@ -1,10 +1,11 @@-{-# LANGUAGE RankNTypes, MultiWayIf, ScopedTypeVariables, CPP, QuasiQuotes, RecordWildCards #-}--- |+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}  module Test.Sandwich.WebDriver.Internal.Screenshots where  import Control.Concurrent-import Control.Exception.Lifted import Control.Monad import Control.Monad.IO.Class import qualified Data.Map as M@@ -15,13 +16,14 @@ import System.FilePath import Test.Sandwich.WebDriver.Internal.Types import Test.WebDriver+import UnliftIO.Exception  saveScreenshots :: (HasCallStack) => T.Text -> WebDriver -> FilePath -> IO () saveScreenshots screenshotName (WebDriver {..}) resultsDir = do   -- For every session, and for every window, try to get a screenshot for the results dir   sessionMap <- readMVar wdSessionMap-  forM_ (M.toList sessionMap) $ \(browser, sess) -> runWD sess $+  forM_ (M.toList sessionMap) $ \(browser, sess) ->     handle (\(e :: HttpException) -> case e of                (HttpExceptionRequest _ content) -> liftIO $ putStrLn [i|HttpException when trying to take a screenshot: '#{content}'|]-               e -> liftIO $ putStrLn [i|HttpException when trying to take a screenshot: '#{e}'|])-           (saveScreenshot $ resultsDir </> [i|#{browser}_#{screenshotName}.png|])+               e' -> liftIO $ putStrLn [i|HttpException when trying to take a screenshot: '#{e'}'|])+           (runWD sess $ saveScreenshot $ resultsDir </> [i|#{browser}_#{screenshotName}.png|])
src/Test/Sandwich/WebDriver/Internal/StartWebDriver.hs view
@@ -1,66 +1,58 @@-{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE CPP #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-}-{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE OverloadedLists #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}  module Test.Sandwich.WebDriver.Internal.StartWebDriver where -import Control.Concurrent-import Control.Exception import Control.Monad import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Control.Monad.Logger-import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.Reader import Control.Retry-import qualified Data.Aeson as A import Data.Default-import Data.Function-import qualified Data.List as L-import Data.Maybe+import Data.Function (fix) import Data.String.Interpolate import qualified Data.Text as T-import qualified Data.Text.IO as T-import qualified Data.Vector as V import GHC.Stack-import Lens.Micro-import Lens.Micro.Aeson import System.Directory import System.FilePath-import System.IO-import System.Process+import System.IO (hClose, hGetLine) import Test.Sandwich-import Test.Sandwich.WebDriver.Internal.Binaries-import Test.Sandwich.WebDriver.Internal.Binaries.DetectChrome (detectChromeVersion)-import Test.Sandwich.WebDriver.Internal.Ports+import Test.Sandwich.Contexts.Files+import Test.Sandwich.Contexts.Util.Ports (findFreePortOrException)+import Test.Sandwich.Util.Process+import Test.Sandwich.WebDriver.Internal.Capabilities.Extra+import Test.Sandwich.WebDriver.Internal.Dependencies import Test.Sandwich.WebDriver.Internal.Types import Test.Sandwich.WebDriver.Internal.Util import qualified Test.WebDriver as W-import qualified Test.WebDriver.Firefox.Profile as FF+import UnliftIO.Async+import UnliftIO.Concurrent+import UnliftIO.Exception+import UnliftIO.Process+import UnliftIO.Timeout  #ifndef mingw32_HOST_OS import Test.Sandwich.WebDriver.Internal.StartWebDriver.Xvfb #endif -#if MIN_VERSION_aeson(2,0,0)-import qualified Data.Aeson.Key             as A-import qualified Data.Aeson.KeyMap          as HM-fromText :: T.Text -> A.Key-fromText = A.fromText-#else-import qualified Data.HashMap.Strict        as HM-fromText :: T.Text -> T.Text-fromText = id-#endif --type Constraints m = (HasCallStack, MonadLogger m, MonadIO m, MonadBaseControl IO m, MonadMask m)+type Constraints m = (+  HasCallStack, MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadFail m+  )  -- | Spin up a Selenium WebDriver and create a WebDriver-startWebDriver :: Constraints m => WdOptions -> FilePath -> m WebDriver-startWebDriver wdOptions@(WdOptions {..}) runRoot = do+startWebDriver :: (+  Constraints m, MonadReader context m, HasBaseContext context+  , HasFile context "java", HasFile context "selenium.jar", HasBrowserDependencies context+  ) => WdOptions -> OnDemandOptions -> FilePath -> m WebDriver+startWebDriver wdOptions@(WdOptions {capabilities=capabilities'', ..}) (OnDemandOptions {..}) runRoot = do   -- Create a unique name for this webdriver so the folder for its log output doesn't conflict with any others   webdriverName <- ("webdriver_" <>) <$> liftIO makeUUID @@ -68,97 +60,92 @@   let webdriverRoot = runRoot </> (T.unpack webdriverName)   liftIO $ createDirectoryIfMissing True webdriverRoot +  -- Directory to hold any downloads   let downloadDir = webdriverRoot </> "Downloads"   liftIO $ createDirectoryIfMissing True downloadDir -  -- Get selenium and chromedriver-  debug [i|Preparing to create the Selenium process|]-  seleniumPath <- obtainSelenium toolsRoot seleniumToUse >>= \case-    Left err -> error [i|Failed to obtain selenium: '#{err}'|]-    Right p -> return p-  driverArgs <- case W.browser capabilities of-    W.Firefox {} -> do-      obtainGeckoDriver toolsRoot geckoDriverToUse >>= \case-        Left err -> error [i|Failed to obtain geckodriver: '#{err}'|]-        Right p -> return [[i|-Dwebdriver.gecko.driver=#{p}|]-                          -- , [i|-Dwebdriver.gecko.logfile=#{webdriverRoot </> "geckodriver.log"}|]-                          -- , [i|-Dwebdriver.gecko.verboseLogging=true|]-                          ]-    W.Chrome {} -> do-      obtainChromeDriver toolsRoot chromeDriverToUse >>= \case-        Left err -> error [i|Failed to obtain chromedriver: '#{err}'|]-        Right p -> return [[i|-Dwebdriver.chrome.driver=#{p}|]-                          , [i|-Dwebdriver.chrome.logfile=#{webdriverRoot </> "chromedriver.log"}|]-                          , [i|-Dwebdriver.chrome.verboseLogging=true|]]-    x -> error [i|Browser #{x} is not supported yet|]--  debug [i|driverArgs: #{driverArgs}|]+  -- Get selenium, driver args, and capabilities with browser paths applied+  java <- askFile @"java"+  seleniumPath <- askFile @"selenium.jar"+  (driverArgs, capabilities') <- fillInCapabilitiesAndGetDriverArgs webdriverRoot capabilities'' +  -- Set up xvfb if configured+  xvfbOnDemand <- newMVar OnDemandNotStarted   (maybeXvfbSession, javaEnv) <- case runMode of #ifndef mingw32_HOST_OS     RunInXvfb (XvfbConfig {..}) -> do-      (s, e) <- makeXvfbSession xvfbResolution xvfbStartFluxbox webdriverRoot+      (s, e) <- makeXvfbSession xvfbResolution xvfbStartFluxbox webdriverRoot xvfbToUse xvfbOnDemand       return (Just s, Just e) #endif     _ -> return (Nothing, Nothing) +  -- Create a distinct process name+  webdriverProcessName <- ("webdriver_process_" <>) <$> (liftIO makeUUID)+  let webdriverProcessRoot = webdriverRoot </> T.unpack webdriverProcessName+  liftIO $ createDirectoryIfMissing True webdriverProcessRoot+   -- Retry up to 10 times   -- This is necessary because sometimes we get a race for the port we get from findFreePortOrException.   -- There doesn't seem to be any way to make Selenium choose its own port.   let policy = constantDelay 0 <> limitRetries 10-  recoverAll policy $ \retryStatus -> do+  (port, hRead, p) <- recoverAll policy $ \retryStatus -> flip withException (\(e :: SomeException) -> warn [i|Exception in startWebDriver retry: #{e}|]) $ do     when (rsIterNumber retryStatus > 0) $-      warn [i|Trying again to start selenium server|]+      warn [i|Trying again to start selenium server (attempt #{rsIterNumber retryStatus})|] -    -- Create a distinct process name-    webdriverProcessName <- ("webdriver_process_" <>) <$> (liftIO makeUUID)-    let webdriverProcessRoot = webdriverRoot </> T.unpack webdriverProcessName-    liftIO $ createDirectoryIfMissing True webdriverProcessRoot-    startWebDriver' wdOptions webdriverName webdriverProcessRoot downloadDir seleniumPath driverArgs maybeXvfbSession javaEnv+    (hRead, hWrite) <- createPipe+    port <- findFreePortOrException -startWebDriver' wdOptions@(WdOptions {capabilities=capabilities', ..}) webdriverName webdriverRoot downloadDir seleniumPath driverArgs maybeXvfbSession javaEnv = do-  port <- liftIO findFreePortOrException-  let wdCreateProcess = (proc "java" (driverArgs <> ["-jar", seleniumPath-                                                    , "-port", show port])) { env = javaEnv }+    let allArgs = driverArgs <> ["-jar", seleniumPath+                                , "-port", show port]+    let cp = (proc java allArgs) {+               env = javaEnv+               , std_in = Inherit+               , std_out = UseHandle hWrite+               , std_err = UseHandle hWrite+               , create_group = True+             } -  -- Open output handles-  let seleniumOutPath = webdriverRoot </> seleniumOutFileName-  hout <- liftIO $ openFile seleniumOutPath AppendMode-  let seleniumErrPath = webdriverRoot </> seleniumErrFileName-  herr <- liftIO $ openFile seleniumErrPath AppendMode+    -- Start the process and wait for it to be ready+    debug [i|#{java} #{T.unwords $ fmap T.pack allArgs}|] -  -- Start the process and wait for it to be ready-  debug [i|Starting the Selenium process|]-  (_, _, _, p) <- liftIO $ createProcess $ wdCreateProcess {-    std_in = Inherit-    , std_out = UseHandle hout-    , std_err = UseHandle herr-    , create_group = True-    }-  -- Normally Selenium prints the ready message to stderr. However, when we're running under-  -- XVFB the two streams get combined and sent to stdout; see-  -- https://bugs.launchpad.net/ubuntu/+source/xorg-server/+bug/1059947-  -- As a result, we poll both files-  let readyMessage = "Selenium Server is up and running"-  -- Retry every 60ms, for up to 60s before admitting defeat-  let policy = constantDelay 60000 <> limitRetries 1000-  success <- retrying policy (\_retryStatus result -> return (not result)) $ const $-    liftIO (T.readFile seleniumErrPath) >>= \case-      t | readyMessage `T.isInfixOf` t -> return True-      _ -> liftIO (T.readFile seleniumOutPath) >>= \case-        t | readyMessage `T.isInfixOf` t -> return True-        _ -> return False-  unless success $ liftIO $ do-    interruptProcessGroupOf p >> waitForProcess p-    error [i|Selenium server failed to start after 60 seconds|]+    (_, _, _, p) <- liftIO $ createProcess cp -  capabilities <- configureHeadlessCapabilities wdOptions runMode capabilities'-                  >>= configureDownloadCapabilities downloadDir+    let teardown = do+          gracefullyStopProcess p 30_000_000+          liftIO $ hClose hRead +    -- On exception, make sure the process is gone and the pipe handle is closed+    flip withException (\(_ :: SomeException) -> teardown) $ do+      -- Read from the (combined) output stream until we see the up and running message,+      -- or the process ends and we get an exception from hGetLine+      startupResult <- timeout 10_000_000 $ fix $ \loop -> do+        line <- fmap T.pack $ liftIO $ hGetLine hRead+        debug line++        if | "Selenium Server is up and running" `T.isInfixOf` line -> return ()+           | otherwise -> loop++      case startupResult of+        Nothing -> do+          let msg = [i|Didn't see "up and running" line in Selenium output after 10s.|]+          warn msg+          expectationFailure (T.unpack msg)+        Just () -> return (port, hRead, p)++  -- TODO: save this in the WebDriver to tear it down later?+  _logAsync <- async $ forever (liftIO (hGetLine hRead) >>= (debug . T.pack))++  -- Final extra capabilities configuration+  capabilities <-+    configureHeadlessCapabilities wdOptions runMode capabilities'+    >>= configureDownloadCapabilities downloadDir+   -- Make the WebDriver   WebDriver <$> pure (T.unpack webdriverName)-            <*> pure (hout, herr, p, seleniumOutPath, seleniumErrPath, maybeXvfbSession)-            <*> pure wdOptions+            <*> pure (p, maybeXvfbSession)+            <*> pure (wdOptions {+                       capabilities = capabilities+                     })             <*> liftIO (newMVar mempty)             <*> pure (def { W.wdPort = fromIntegral port                           , W.wdCapabilities = capabilities@@ -167,103 +154,23 @@                           })             <*> pure downloadDir --- | TODO: expose this as an option-gracePeriod :: Int-gracePeriod = 30000000+            <*> pure ffmpegToUse+            <*> newMVar OnDemandNotStarted +            <*> pure xvfbToUse+            <*> pure xvfbOnDemand++ stopWebDriver :: Constraints m => WebDriver -> m ()-stopWebDriver (WebDriver {wdWebDriver=(hout, herr, h, _, _, maybeXvfbSession)}) = do+stopWebDriver (WebDriver {wdWebDriver=(h, maybeXvfbSession)}) = do+  -- | TODO: expose this as an option+  let gracePeriod :: Int+      gracePeriod = 30000000+   gracefullyStopProcess h gracePeriod-  liftIO $ hClose hout-  liftIO $ hClose herr    whenJust maybeXvfbSession $ \(XvfbSession {..}) -> do     whenJust xvfbFluxboxProcess $ \p -> do       gracefullyStopProcess p gracePeriod      gracefullyStopProcess xvfbProcess gracePeriod---- * Util--seleniumOutFileName, seleniumErrFileName :: FilePath-seleniumOutFileName = "stdout.txt"-seleniumErrFileName = "stderr.txt"---- | Add headless configuration to the Chrome browser-configureHeadlessCapabilities :: Constraints m => WdOptions -> RunMode -> W.Capabilities -> m W.Capabilities-configureHeadlessCapabilities wdOptions (RunHeadless (HeadlessConfig {..})) caps@(W.Capabilities {W.browser=browser@(W.Chrome {..})}) = do-  headlessArg <- liftIO (detectChromeVersion (chromeBinaryPath wdOptions)) >>= \case-    Left err -> do-      warn [i|Couldn't determine chrome version when configuring headless capabilities (err: #{err}); passing --headless|]-      return "--headless"-    Right (ChromeVersion (major, _, _, _))-      -- See https://www.selenium.dev/blog/2023/headless-is-going-away/-      | major >= 110 -> return "--headless=new"-      | otherwise -> return "--headless"--  let browser' = browser { W.chromeOptions = headlessArg:resolution:chromeOptions }--  return (caps { W.browser = browser' })--  where-    resolution = [i|--window-size=#{w},#{h}|]-    (w, h) = fromMaybe (1920, 1080) headlessResolution---- | Add headless configuration to the Firefox capabilities-configureHeadlessCapabilities _ (RunHeadless (HeadlessConfig {..})) caps@(W.Capabilities {W.browser=(W.Firefox {..}), W.additionalCaps=ac}) = return (caps { W.additionalCaps = additionalCaps })-  where-    additionalCaps = case L.findIndex (\x -> fst x == "moz:firefoxOptions") ac of-      Nothing -> ("moz:firefoxOptions", A.object [("args", A.Array ["-headless"])]) : ac-      Just i -> let ffOptions' = snd (ac !! i)-                               & ensureKeyExists "args" (A.Array [])-                               & ((key "args" . _Array) %~ addHeadlessArg) in-        L.nubBy (\x y -> fst x == fst y) (("moz:firefoxOptions", ffOptions') : ac)--    ensureKeyExists :: T.Text -> A.Value -> A.Value -> A.Value-    ensureKeyExists key _ val@(A.Object (HM.lookup (fromText key) -> Just _)) = val-    ensureKeyExists key defaultVal (A.Object m@(HM.lookup (fromText key) -> Nothing)) = A.Object (HM.insert (fromText key) defaultVal m)-    ensureKeyExists _ _ _ = error "Expected Object in ensureKeyExists"--    addHeadlessArg :: V.Vector A.Value -> V.Vector A.Value-    addHeadlessArg xs | (A.String "-headless") `V.elem` xs = xs-    addHeadlessArg xs = (A.String "-headless") `V.cons` xs--configureHeadlessCapabilities _ (RunHeadless {}) browser = error [i|Headless mode not yet supported for browser '#{browser}'|]-configureHeadlessCapabilities _ _ browser = return browser---configureDownloadCapabilities downloadDir caps@(W.Capabilities {W.browser=browser@(W.Firefox {..})}) = do-  case ffProfile of-    Nothing -> return ()-    Just _ -> liftIO $ throwIO $ userError [i|Can't support Firefox profile yet.|]--  profile <- FF.defaultProfile-    & FF.addPref "browser.download.folderList" (2 :: Int)-    & FF.addPref "browser.download.manager.showWhenStarting" False-    & FF.addPref "browser.download.dir" downloadDir-    & FF.addPref "browser.helperApps.neverAsk.saveToDisk" ("*" :: String)-    & FF.prepareProfile--  return (caps { W.browser = browser { W.ffProfile = Just profile } })-configureDownloadCapabilities downloadDir caps@(W.Capabilities {W.browser=browser@(W.Chrome {..})}) = return $ caps { W.browser=browser' }-  where-    browser' = browser { W.chromeExperimentalOptions = options }--    basePrefs :: A.Object-    basePrefs = case HM.lookup "prefs" chromeExperimentalOptions of-      Just (A.Object hm) -> hm-      Just x -> error [i|Expected chrome prefs to be object, got '#{x}'.|]-      Nothing -> mempty--    prefs :: A.Object-    prefs = basePrefs-          & foldl (.) id [HM.insert k v | (k, v) <- downloadPrefs]--    options = HM.insert "prefs" (A.Object prefs) chromeExperimentalOptions--    downloadPrefs = [("profile.default_content_setting_values.automatic_downloads", A.Number 1)-                    , ("profile.content_settings.exceptions.automatic_downloads.*.setting", A.Number 1)-                    , ("download.prompt_for_download", A.Bool False)-                    , ("download.directory_upgrade", A.Bool True)-                    , ("download.default_directory", A.String (T.pack downloadDir))]-configureDownloadCapabilities _ browser = return browser
src/Test/Sandwich/WebDriver/Internal/Types.hs view
@@ -12,30 +12,26 @@ import Data.IORef import qualified Data.Map as M import Data.String.Interpolate-import Data.Text as T+import Data.Text (Text) import Network.HTTP.Client (Manager)-import System.IO import System.Process import Test.Sandwich+import Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg+import Test.Sandwich.WebDriver.Internal.Binaries.Xvfb import qualified Test.WebDriver as W-import qualified Test.WebDriver.Class as W import qualified Test.WebDriver.Session as W+import UnliftIO.Async + -- | 'Session' is just a 'String' name. type Session = String  -- * Labels-webdriver = Label :: Label "webdriver" WebDriver-webdriverSession = Label :: Label "webdriverSession" WebDriverSession--type WebDriverContext context wd = (HasLabel context "webdriver" WebDriver, W.WebDriver (ExampleT context wd))---- TODO: remove-class HasWebDriver a where-  getWebDriver :: a -> WebDriver+webdriver :: Label "webdriver" WebDriver+webdriver = Label -instance HasWebDriver WebDriver where-  getWebDriver = id+webdriverSession :: Label "webdriverSession" WebDriverSession+webdriverSession = Label  type ToolsRoot = FilePath @@ -44,37 +40,22 @@ -- | Headless and Xvfb modes are useful because they allow you to run tests in the background, without popping up browser windows. -- This is useful for development or for running on a CI server, and is also more reproducible since the screen resolution can be fixed. -- In addition, Xvfb mode allows videos to be recorded of tests.-data RunMode = Normal-             -- ^ Normal Selenium behavior; will pop up a web browser.-             | RunHeadless HeadlessConfig-             -- ^ Run with a headless browser. Supports screenshots but videos will be black.-             | RunInXvfb XvfbConfig-             -- ^ Run inside <https://en.wikipedia.org/wiki/Xvfb Xvfb> so that tests run in their own X11 display.-             -- xvfb-run script must be installed and on the PATH.+data RunMode =+  Normal+  -- ^ Normal Selenium behavior; will pop up a web browser.+  | RunHeadless HeadlessConfig+  -- ^ Run with a headless browser. Supports screenshots but videos will be black.+  | RunInXvfb XvfbConfig+  -- ^ Run inside <https://en.wikipedia.org/wiki/Xvfb Xvfb> so that tests run in their own X11 display.+  -- The @Xvfb@ binary must be installed and on the PATH.  data WdOptions = WdOptions {-  toolsRoot :: ToolsRoot-  -- ^ Folder where any necessary binaries (chromedriver, Selenium, etc.) will be downloaded if needed. Required.--  , capabilities :: W.Capabilities+  capabilities :: W.Capabilities   -- ^ The WebDriver capabilities to use.    , saveSeleniumMessageHistory :: WhenToSave   -- ^ When to save a record of Selenium requests and responses. -  , seleniumToUse :: SeleniumToUse-  -- ^ Which Selenium server JAR file to use.--  , chromeBinaryPath :: Maybe FilePath-  -- ^ Which chrome binary to use.-  , chromeDriverToUse :: ChromeDriverToUse-  -- ^ Which chromedriver executable to use.--  , firefoxBinaryPath :: Maybe FilePath-  -- ^ Which firefox binary to use.-  , geckoDriverToUse :: GeckoDriverToUse-  -- ^ Which geckodriver executable to use.-   , runMode :: RunMode   -- ^ How to handle opening the browser (in a popup window, headless, etc.). @@ -85,57 +66,28 @@   -- ^ Number of times to retry an HTTP request if it times out.   } --- | How to obtain the Selenium server JAR file.-data SeleniumToUse =-  DownloadSeleniumFrom String-  -- ^ Download selenium from the given URL to the 'toolsRoot'-  | DownloadSeleniumDefault-  -- ^ Download selenium from a default location to the 'toolsRoot'-  | UseSeleniumAt FilePath-  -- ^ Use the JAR file at the given path-  deriving Show---- | How to obtain the chromedriver binary.-data ChromeDriverToUse =-  DownloadChromeDriverFrom String-  -- ^ Download chromedriver from the given URL to the 'toolsRoot'-  | DownloadChromeDriverVersion ChromeDriverVersion-  -- ^ Download the given chromedriver version to the 'toolsRoot'-  | DownloadChromeDriverAutodetect (Maybe FilePath)-  -- ^ Autodetect chromedriver to use based on the Chrome version and download it to the 'toolsRoot'-  -- Pass the path to the Chrome binary, or else it will be found by looking for google-chrome on the PATH.-  | UseChromeDriverAt FilePath-  -- ^ Use the chromedriver at the given path-  deriving Show---- | How to obtain the geckodriver binary.-data GeckoDriverToUse =-  DownloadGeckoDriverFrom String-  -- ^ Download geckodriver from the given URL to the 'toolsRoot'-  | DownloadGeckoDriverVersion GeckoDriverVersion-  -- ^ Download the given geckodriver version to the 'toolsRoot'-  | DownloadGeckoDriverAutodetect (Maybe FilePath)-  -- ^ Autodetect geckodriver to use based on the Gecko version and download it to the 'toolsRoot'-  -- Pass the path to the Firefox binary, or else it will be found by looking for firefox on the PATH.-  | UseGeckoDriverAt FilePath-  -- ^ Use the geckodriver at the given path-  deriving Show--newtype ChromeVersion = ChromeVersion (Int, Int, Int, Int) deriving Show-data ChromeDriverVersion =-  ChromeDriverVersionTuple (Int, Int, Int, Int)-  | ChromeDriverVersionExactUrl (Int, Int, Int, Int) Text-  deriving Show+-- | How to obtain certain binaries "on demand". These may or not be needed based on 'WdOptions', so+-- they will be obtained as needed.+data OnDemandOptions = OnDemandOptions {+  -- | How to obtain ffmpeg binary.+  ffmpegToUse :: FfmpegToUse -newtype FirefoxVersion = FirefoxVersion (Int, Int, Int) deriving Show-newtype GeckoDriverVersion = GeckoDriverVersion (Int, Int, Int) deriving Show+  -- | How to obtain Xvfb binary.+  , xvfbToUse :: XvfbToUse+  }+defaultOnDemandOptions = OnDemandOptions {+  ffmpegToUse = UseFfmpegFromPath+  , xvfbToUse = UseXvfbFromPath+  } +-- | Configuration for a headless browser. data HeadlessConfig = HeadlessConfig {   headlessResolution :: Maybe (Int, Int)-  -- ^ Resolution for the headless browser. Defaults to (1920, 1080)+  -- ^ Resolution for the headless browser, specified as @(width, height)@. Defaults to @(1920, 1080)@.   }  -- | Default headless config.+defaultHeadlessConfig :: HeadlessConfig defaultHeadlessConfig = HeadlessConfig Nothing  data XvfbConfig = XvfbConfig {@@ -143,36 +95,43 @@   -- ^ Resolution for the virtual screen. Defaults to (1920, 1080)    , xvfbStartFluxbox :: Bool-  -- ^ Whether to start fluxbox window manager to go with the Xvfb session. fluxbox must be on the path+  -- ^ Whether to start fluxbox window manager to go with the Xvfb session. @fluxbox@ must be on the path.   }  -- | Default Xvfb settings.+defaultXvfbConfig :: XvfbConfig defaultXvfbConfig = XvfbConfig Nothing False  -- | The default 'WdOptions' object. -- You should start with this and modify it using the accessors.-defaultWdOptions :: FilePath -> WdOptions-defaultWdOptions toolsRoot = WdOptions {-  toolsRoot = toolsRoot-  , capabilities = def+defaultWdOptions :: WdOptions+defaultWdOptions = WdOptions {+  capabilities = def   , saveSeleniumMessageHistory = OnException-  , seleniumToUse = DownloadSeleniumDefault-  , chromeBinaryPath = Nothing-  , chromeDriverToUse = DownloadChromeDriverAutodetect Nothing-  , firefoxBinaryPath = Nothing-  , geckoDriverToUse = DownloadGeckoDriverAutodetect Nothing   , runMode = Normal   , httpManager = Nothing   , httpRetryCount = 0   } +data OnDemand a =+  OnDemandNotStarted+  | OnDemandInProgress (Async a)+  | OnDemandReady a+  | OnDemandErrored Text+ data WebDriver = WebDriver {   wdName :: String-  , wdWebDriver :: (Handle, Handle, ProcessHandle, FilePath, FilePath, Maybe XvfbSession)+  , wdWebDriver :: (ProcessHandle, Maybe XvfbSession)   , wdOptions :: WdOptions   , wdSessionMap :: MVar (M.Map Session W.WDSession)   , wdConfig :: W.WDConfig   , wdDownloadDir :: FilePath++  , wdFfmpegToUse :: FfmpegToUse+  , wdFfmpeg :: MVar (OnDemand FilePath)++  , wdXvfbToUse :: XvfbToUse+  , wdXvfb :: MVar (OnDemand FilePath)   }  data InvalidLogsException = InvalidLogsException [W.LogEntry]@@ -180,11 +139,13 @@  instance Exception InvalidLogsException -data XvfbSession = XvfbSession { xvfbDisplayNum :: Int-                               , xvfbXauthority :: FilePath-                               , xvfbDimensions :: (Int, Int)-                               , xvfbProcess :: ProcessHandle-                               , xvfbFluxboxProcess :: Maybe ProcessHandle }+data XvfbSession = XvfbSession {+  xvfbDisplayNum :: Int+  , xvfbXauthority :: FilePath+  , xvfbDimensions :: (Int, Int)+  , xvfbProcess :: ProcessHandle+  , xvfbFluxboxProcess :: Maybe ProcessHandle+  }  type WebDriverSession = (Session, IORef W.WDSession) @@ -195,67 +156,22 @@ -- | Get the X11 display number associated with the 'WebDriver'. -- Only present if running in 'RunInXvfb' mode. getDisplayNumber :: WebDriver -> Maybe Int-getDisplayNumber (WebDriver {wdWebDriver=(_, _, _, _, _, Just (XvfbSession {xvfbDisplayNum}))}) = Just xvfbDisplayNum+getDisplayNumber (WebDriver {wdWebDriver=(_, Just (XvfbSession {xvfbDisplayNum}))}) = Just xvfbDisplayNum getDisplayNumber _ = Nothing  -- | Get the Xvfb session associated with the 'WebDriver', if present. getXvfbSession :: WebDriver -> Maybe XvfbSession-getXvfbSession (WebDriver {wdWebDriver=(_, _, _, _, _, Just sess)}) = Just sess+getXvfbSession (WebDriver {wdWebDriver=(_, Just sess)}) = Just sess getXvfbSession _ = Nothing +-- | Get the configured download directory for the 'WebDriver'.+getDownloadDirectory :: WebDriver -> FilePath+getDownloadDirectory = wdDownloadDir+ -- | Get the name of the 'WebDriver'.+-- This corresponds to the folder that will be created to hold the log files for the 'WebDriver'. getWebDriverName :: WebDriver -> String getWebDriverName (WebDriver {wdName}) = wdName  instance Show XvfbSession where   show (XvfbSession {xvfbDisplayNum}) = [i|<XVFB session with server num #{xvfbDisplayNum}>|]---- * Video stuff---- | Default options for fast X11 video recording.-fastX11VideoOptions = ["-an"-                      , "-r", "30"-                      , "-vcodec"-                      , "libxvid"-                      , "-qscale:v", "1"-                      , "-threads", "0"]---- | Default options for quality X11 video recording.-qualityX11VideoOptions = ["-an"-                         , "-r", "30"-                         , "-vcodec", "libx264"-                         , "-preset", "veryslow"-                         , "-crf", "0"-                         , "-threads", "0"]---- | Default options for AVFoundation recording (for Darwin).-defaultAvfoundationOptions = ["-r", "30"-                             , "-an"-                             , "-vcodec", "libxvid"-                             , "-qscale:v", "1"-                             , "-threads", "0"]---- | Default options for gdigrab recording (for Windows).-defaultGdigrabOptions = ["-framerate", "30"]--data VideoSettings = VideoSettings {-  x11grabOptions :: [String]-  -- ^ Arguments to x11grab, used with Linux.-  , avfoundationOptions :: [String]-  -- ^ Arguments to avfoundation, used with OS X.-  , gdigrabOptions :: [String]-  -- ^ Arguments to gdigrab, used with Windows.-  , hideMouseWhenRecording :: Bool-  -- ^ Hide the mouse while recording video. Linux and Windows only.-  , logToDisk :: Bool-  -- ^ Log ffmpeg stdout and stderr to disk.-  }---- | Default video settings.-defaultVideoSettings = VideoSettings {-  x11grabOptions = fastX11VideoOptions-  , avfoundationOptions = defaultAvfoundationOptions-  , gdigrabOptions = defaultGdigrabOptions-  , hideMouseWhenRecording = False-  , logToDisk = True-  }
src/Test/Sandwich/WebDriver/Internal/Util.hs view
@@ -2,55 +2,27 @@  module Test.Sandwich.WebDriver.Internal.Util where -import Control.Exception-import qualified Control.Exception.Lifted as E import Control.Monad-import Control.Monad.IO.Class-import Control.Monad.Logger-import Control.Monad.Trans.Control (MonadBaseControl)-import Control.Retry-import Data.Maybe-import Data.String.Interpolate+import Control.Monad.IO.Unlift import qualified Data.Text as T-import System.Directory-import System.Process import qualified System.Random as R-import Test.Sandwich.Logging--#ifdef mingw32_HOST_OS-import System.IO-#endif+import Test.Sandwich (expectationFailure)+import UnliftIO.Exception  --- * Truncating log files--moveAndTruncate :: FilePath -> String -> IO ()-moveAndTruncate from to = do-  exists <- doesFileExist from-  when exists $ do-    copyFile from to-    tryTruncateFile from--  where-    tryTruncateFile :: FilePath -> IO ()-    tryTruncateFile path = E.catch (truncateFile path)-                                   (\(e :: E.SomeException) -> putStrLn [i|Failed to truncate file #{path}: #{e}|])--    truncateFile :: FilePath -> IO ()-#ifdef mingw32_HOST_OS-    truncateFile path = withFile path WriteMode $ flip hPutStr "\n" -- Not exactly truncation, but close enough?-#else-    truncateFile path = void $ readCreateProcess (shell [i|> #{path}|]) ""-#endif- -- * Exceptions -leftOnException :: (MonadIO m, MonadBaseControl IO m) => m (Either T.Text a) -> m (Either T.Text a)-leftOnException = E.handle (\(e :: SomeException) -> return $ Left $ T.pack $ show e)+leftOnException :: (MonadUnliftIO m) => m (Either T.Text a) -> m (Either T.Text a)+leftOnException = handle (\(e :: SomeException) -> return $ Left $ T.pack $ show e) -leftOnException' :: (MonadIO m, MonadBaseControl IO m) => m a -> m (Either T.Text a)-leftOnException' action = E.catch (Right <$> action) (\(e :: SomeException) -> return $ Left $ T.pack $ show e)+exceptionOnLeft :: (MonadUnliftIO m) => m (Either T.Text a) -> m a+exceptionOnLeft action = action >>= \case+  Left err -> expectationFailure (T.unpack err)+  Right x -> pure x +leftOnException' :: (MonadUnliftIO m) => m a -> m (Either T.Text a)+leftOnException' action = catch (Right <$> action) (\(e :: SomeException) -> return $ Left $ T.pack $ show e)+ -- * Util  whenJust :: (Monad m) => Maybe a -> (a -> m b) -> m ()@@ -67,31 +39,3 @@  makeUUID :: IO T.Text makeUUID = (T.pack . take 10 . R.randomRs ('a','z')) <$> R.newStdGen---- * Stopping processes--gracefullyStopProcess :: (MonadIO m, MonadLogger m) => ProcessHandle -> Int -> m ()-gracefullyStopProcess p gracePeriodUs = do-  liftIO $ interruptProcessGroupOf p-  gracefullyWaitForProcess p gracePeriodUs--gracefullyWaitForProcess :: (MonadIO m, MonadLogger m) => ProcessHandle -> Int -> m ()-gracefullyWaitForProcess p gracePeriodUs = do-  let waitForExit = do-        let policy = limitRetriesByCumulativeDelay gracePeriodUs $ capDelay 200_000 $ exponentialBackoff 1_000-        retrying policy (\_ x -> return $ isNothing x) $ \_ -> do-          liftIO $ getProcessExitCode p--  waitForExit >>= \case-    Just _ -> return ()-    Nothing -> do-      pid <- liftIO $ getPid p-      warn [i|(#{pid}) Process didn't stop after #{gracePeriodUs}us; trying to interrupt|]--      liftIO $ interruptProcessGroupOf p-      waitForExit >>= \case-        Just _ -> return ()-        Nothing -> void $ do-          warn [i|(#{pid}) Process didn't stop after a further #{gracePeriodUs}us; going to kill|]-          liftIO $ terminateProcess p-          liftIO $ waitForProcess p
− src/Test/Sandwich/WebDriver/Internal/Video.hs
@@ -1,65 +0,0 @@-{-# LANGUAGE CPP #-}--module Test.Sandwich.WebDriver.Internal.Video where--import Control.Monad.IO.Class-import Data.Maybe-import Data.String.Interpolate-import System.Environment-import System.Process-import Test.Sandwich.WebDriver.Internal.Types--#ifdef darwin_HOST_OS-import Safe--getMacScreenNumber :: IO (Maybe Int)-getMacScreenNumber = undefined-#endif---getVideoArgs path (width, height, x, y) (VideoSettings {..}) maybeXvfbSession = do-#ifdef linux_HOST_OS-  displayNum <- case maybeXvfbSession of-    Nothing -> fromMaybe "" <$> (liftIO $ lookupEnv "DISPLAY")-    Just (XvfbSession {xvfbDisplayNum}) -> return $ ":" <> show xvfbDisplayNum--  let videoPath = [i|#{path}.avi|]-  let env' = [("DISPLAY", displayNum)]-  let env = case maybeXvfbSession of-       Nothing -> Just env'-       Just (XvfbSession {xvfbXauthority}) -> Just (("XAUTHORITY", xvfbXauthority) : env')-  let cmd = ["-draw_mouse", (if hideMouseWhenRecording then "0" else "1")-            , "-y"-            , "-nostdin"-            , "-f", "x11grab"-            , "-s", [i|#{width}x#{height}|]-            , "-i", [i|#{displayNum}.0+#{x},#{y}|]]-            ++ x11grabOptions-            ++ [videoPath]-  return ((proc "ffmpeg" cmd) { env = env })-#endif--#ifdef darwin_HOST_OS-  maybeScreenNumber <- liftIO getMacScreenNumber-  let videoPath = [i|#{path}.avi|]-  let cmd = case maybeScreenNumber of-        Just screenNumber -> ["-y"-                             , "-nostdin"-                             , "-f", "avfoundation"-                             , "-i", [i|#{screenNumber}|]]-                             ++ avfoundationOptions-                             ++ [videoPath]-        Nothing -> error [i|Not launching ffmpeg since OS X screen number couldn't be determined.|]-  return ((proc "ffmpeg" cmd) { env = Nothing })-#endif--#ifdef mingw32_HOST_OS-  let videoPath = [i|#{path}.mkv|]-  let cmd = ["-f", "gdigrab"-            , "-nostdin"-            , "-draw_mouse", (if hideMouseWhenRecording then "0" else "1")-            , "-i", "desktop"]-            ++ gdigrabOptions-            ++ [videoPath]-  return ((proc "ffmpeg.exe" cmd) { env = Nothing })-#endif
src/Test/Sandwich/WebDriver/Types.hs view
@@ -3,43 +3,44 @@ {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}  module Test.Sandwich.WebDriver.Types (-  ExampleWithWebDriver+  -- * Type aliases to make signatures shorter+  BaseMonad+  , ContextWithWebdriverDeps+  , ContextWithBaseDeps+  , WebDriverMonad+  , WebDriverSessionMonad++  -- * Context aliases+  , HasBrowserDependencies   , HasWebDriverContext   , HasWebDriverSessionContext-  , ContextWithSession -  , hoistExample--  , webdriver--  , wdDownloadDir--  -- * Constraint synonyms-  , BaseMonad-  , BaseMonadContext-  , WebDriverMonad-  , WebDriverSessionMonad+  -- * The Xvfb session+  , XvfbSession(..)+  , getXvfbSession   ) where -import Control.Exception.Safe as ES+import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Control.Monad.Reader import Control.Monad.Trans.Control (MonadBaseControl) import Data.IORef import GHC.Stack import Test.Sandwich-import Test.Sandwich.Internal+import Test.Sandwich.Contexts.Files+import Test.Sandwich.WebDriver.Internal.Dependencies import Test.Sandwich.WebDriver.Internal.Types import qualified Test.WebDriver.Class as W import qualified Test.WebDriver.Internal as WI import qualified Test.WebDriver.Session as W+import UnliftIO.Exception as ES  -type ContextWithSession context = LabelValue "webdriverSession" WebDriverSession :> context--instance (MonadIO m, HasLabel context "webdriverSession" WebDriverSession) => W.WDSessionState (ExampleT context m) where+instance (MonadIO m, HasWebDriverSessionContext context) => W.WDSessionState (ExampleT context m) where   getSession = do     (_, sessVar) <- getContext webdriverSession     liftIO $ readIORef sessVar@@ -48,7 +49,7 @@     liftIO $ writeIORef sessVar sess  -- Implementation copied from that of the WD monad implementation-instance (MonadIO m, MonadThrow m, HasLabel context "webdriverSession" WebDriverSession, MonadBaseControl IO m) => W.WebDriver (ExampleT context m) where+instance (MonadIO m, MonadBaseControl IO m, HasWebDriverSessionContext context) => W.WebDriver (ExampleT context m) where   doCommand method path args = WI.mkRequest method path args     >>= WI.sendHTTPRequest     >>= either throwIO return@@ -57,13 +58,21 @@  type HasWebDriverContext context = HasLabel context "webdriver" WebDriver type HasWebDriverSessionContext context = HasLabel context "webdriverSession" WebDriverSession-type ExampleWithWebDriver context wd = (W.WDSessionState (ExampleT context wd), W.WebDriver wd) -hoistExample :: ExampleT context IO a -> ExampleT (ContextWithSession context) IO a-hoistExample (ExampleT r) = ExampleT $ transformContext r-  where transformContext = withReaderT (\(_ :> ctx) -> ctx)+type ContextWithWebdriverDeps context =+  LabelValue "webdriver" WebDriver+  :> ContextWithBaseDeps context -type WebDriverMonad m context = (HasCallStack, HasLabel context "webdriver" WebDriver, MonadIO m, MonadBaseControl IO m)-type WebDriverSessionMonad m context = (WebDriverMonad m context, MonadReader context m, HasLabel context "webdriverSession" WebDriverSession)-type BaseMonad m = (HasCallStack, MonadIO m, MonadCatch m, MonadBaseControl IO m, MonadMask m)-type BaseMonadContext m context = (BaseMonad m, HasBaseContext context)+type ContextWithBaseDeps context =+  -- | Browser dependencies+  LabelValue "browserDependencies" BrowserDependencies+  -- | Java+  :> FileValue "java"+  -- | Selenium+  :> FileValue "selenium.jar"+  -- | Base context+  :> context++type BaseMonad m context = (HasCallStack, MonadUnliftIO m, MonadMask m, HasBaseContext context)+type WebDriverMonad m context = (HasCallStack, MonadUnliftIO m, HasWebDriverContext context)+type WebDriverSessionMonad m context = (WebDriverMonad m context, MonadReader context m, HasWebDriverSessionContext context)
src/Test/Sandwich/WebDriver/Video.hs view
@@ -1,13 +1,19 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE TupleSections #-} +-- | Functions for recording videos of browser windows.+ module Test.Sandwich.WebDriver.Video (-  startVideoRecording+  startBrowserVideoRecording+  , startFullScreenVideoRecording++  -- * Lower-level+  , startVideoRecording   , endVideoRecording -  -- * Helpers-  , startFullScreenVideoRecording-  , startBrowserVideoRecording+  -- * Wrap a test to conditionally record video+  , recordVideoIfConfigured    -- * Configuration   , VideoSettings(..)@@ -16,33 +22,59 @@   , qualityX11VideoOptions   , defaultAvfoundationOptions   , defaultGdigrabOptions++  -- * Types+  , VideoProcess+  , videoProcessProcess+  , BaseVideoConstraints   ) where -import Control.Exception.Safe+import Control.Monad+import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Control.Monad.Logger hiding (logError) import Control.Monad.Reader-import Control.Monad.Trans.Control (MonadBaseControl)+import Data.Function import Data.String.Interpolate import System.Exit import System.FilePath import System.IO import System.Process import Test.Sandwich-import Test.Sandwich.WebDriver import Test.Sandwich.WebDriver.Internal.Types-import Test.Sandwich.WebDriver.Internal.Video+import Test.Sandwich.WebDriver.Types+import Test.Sandwich.WebDriver.Video.Internal+import Test.Sandwich.WebDriver.Video.Types import Test.Sandwich.WebDriver.Windows import Test.WebDriver.Class as W import Test.WebDriver.Commands+import UnliftIO.Directory+import UnliftIO.Exception  -type BaseVideoConstraints context m = (MonadLoggerIO m, MonadReader context m, HasWebDriverContext context, MonadBaseControl IO m)+type BaseVideoConstraints context m = (+  MonadLoggerIO m, MonadUnliftIO m, MonadMask m+  , MonadReader context m, HasBaseContext context, HasWebDriverContext context+  ) +-- | A type representing a live video recording process+data VideoProcess = VideoProcess {+  -- | The process handle+  videoProcessProcess :: ProcessHandle+  , videoProcessCreatedFiles :: [FilePath]+  }+-- defaultVideoProcess :: ProcessHandle -> VideoProcess+-- defaultVideoProcess p = VideoProcess p mempty+ -- | Wrapper around 'startVideoRecording' which uses the full screen dimensions. startFullScreenVideoRecording :: (-  BaseVideoConstraints context m, MonadMask m-  ) => FilePath -> VideoSettings -> m ProcessHandle+  BaseVideoConstraints context m+  )+  -- | Output path+  => FilePath+  -> VideoSettings+  -> m VideoProcess startFullScreenVideoRecording path videoSettings = do   sess <- getContext webdriver   let maybeXvfbSession = getXvfbSession sess@@ -55,22 +87,33 @@  -- | Wrapper around 'startVideoRecording' which uses WebDriver to find the rectangle corresponding to the browser. startBrowserVideoRecording :: (-  BaseVideoConstraints context m, MonadThrow m, HasWebDriverSessionContext context, W.WebDriver m-  ) => FilePath -> VideoSettings -> m ProcessHandle+  BaseVideoConstraints context m, W.WebDriver m+  )+  -- | Output path+  => FilePath+  -> VideoSettings+  -> m VideoProcess startBrowserVideoRecording path videoSettings = do   (x, y) <- getWindowPos   (w, h) <- getWindowSize   startVideoRecording path (w, h, x, y) videoSettings --- | Record video to a given path, for a given rectangle specified as (width, height, x, y).+-- | Record video to a given path, for a given screen rectangle. startVideoRecording :: (   BaseVideoConstraints context m-  ) => FilePath -> (Word, Word, Int, Int) -> VideoSettings -> m ProcessHandle+  )+  -- | Output path+  => FilePath+  -- | Rectangle to record, specified as @(width, height, x, y)@+  -> (Word, Word, Int, Int)+  -> VideoSettings+  -- | Returns handle to video process and list of files created+  -> m VideoProcess startVideoRecording path (width, height, x, y) vs = do   sess <- getContext webdriver   let maybeXvfbSession = getXvfbSession sess -  cp' <- liftIO $ getVideoArgs path (width, height, x, y) vs maybeXvfbSession+  (cp', videoPath) <- getVideoArgs path (width, height, x, y) vs maybeXvfbSession   let cp = cp' { create_group = True }    case cmdspec cp of@@ -78,18 +121,22 @@     RawCommand p args -> debug [i|ffmpeg command: #{p} #{unwords args}|]    case logToDisk vs of-    False -> createProcessWithLogging cp+    False -> do+      p <- createProcessWithLogging cp+      return $ VideoProcess p [videoPath]     True -> do-      liftIO $ bracket (openFile (path <.> "stdout" <.> "log") AppendMode) hClose $ \hout ->-        bracket (openFile (path <.> "stderr" <.> "log") AppendMode) hClose $ \herr -> do+      let stdoutPath = path <.> "stdout" <.> "log"+      let stderrPath = path <.> "stderr" <.> "log"+      liftIO $ bracket (openFile stdoutPath AppendMode) hClose $ \hout ->+        bracket (openFile stderrPath AppendMode) hClose $ \herr -> do           (_, _, _, p) <- createProcess (cp { std_out = UseHandle hout, std_err = UseHandle herr })-          return p+          return $ VideoProcess p [videoPath, stdoutPath, stderrPath]  -- | Gracefully stop the 'ProcessHandle' returned by 'startVideoRecording'. endVideoRecording :: (-  MonadLoggerIO m, MonadCatch m-  ) => ProcessHandle -> m ()-endVideoRecording p = do+  MonadLoggerIO m, MonadUnliftIO m+  ) => VideoProcess -> m ()+endVideoRecording (VideoProcess { videoProcessProcess=p }) = do   catchAny (liftIO $ interruptProcessGroupOf p)            (\e -> logError [i|Exception in interruptProcessGroupOf in endVideoRecording: #{e}|]) @@ -101,3 +148,54 @@     ExitFailure 255 -> return ()      ExitFailure n -> debug [i|ffmpeg exited with unexpected exit code #{n}'|]++-- * Wrappers++-- | Record video around a given action, if configured to do so in the 'CommandLineWebdriverOptions'.+--+-- This can be used to record video around individual tests. It can also keep videos only in case of+-- exceptions, deleting them on successful runs.+recordVideoIfConfigured :: (+  BaseVideoConstraints context m, W.WebDriver m, HasSomeCommandLineOptions context+  )+  -- | Session name+  => String+  -> m a+  -> m a+recordVideoIfConfigured browser action = do+  getCurrentFolder >>= \case+    Nothing -> action+    Just folder -> do+      SomeCommandLineOptions (CommandLineOptions {optWebdriverOptions=(CommandLineWebdriverOptions {..})}) <- getSomeCommandLineOptions+      if | optIndividualVideos -> withVideo folder browser action+         | optErrorVideos -> withVideoIfException folder browser action+         | otherwise -> action++withVideo :: (+  BaseVideoConstraints context m, W.WebDriver m+  ) => FilePath -> String -> m a -> m a+withVideo folder browser action = do+  path <- getPathInFolder folder browser+  bracket (startBrowserVideoRecording path defaultVideoSettings) endVideoRecording (const action)++withVideoIfException :: (+  BaseVideoConstraints context m, W.WebDriver m+  ) => FilePath -> String -> m a -> m a+withVideoIfException folder browser action = do+  path <- getPathInFolder folder browser+  tryAny (bracket (startBrowserVideoRecording path defaultVideoSettings)+                  endVideoRecording+                  (\(VideoProcess {..}) -> (videoProcessCreatedFiles, ) <$> action))+    >>= \case+      Right (pathsToRemove, ret) -> do+        info [i|pathsToRemove: #{pathsToRemove}|]+        forM_ pathsToRemove removePathForcibly+        return ret+      Left e -> throwIO e++getPathInFolder :: (MonadUnliftIO m) => [Char] -> String -> m FilePath+getPathInFolder folder browser = flip fix (0 :: Integer) $ \loop n -> do+  let path = folder </> [i|#{browser}_video_#{n}|]+  liftIO (doesFileExist (path <> videoExtension)) >>= \case+    False -> return path+    True -> loop (n + 1)
+ src/Test/Sandwich/WebDriver/Video/Internal.hs view
@@ -0,0 +1,101 @@+{-# OPTIONS_GHC -fno-warn-unused-matches #-}+{-# LANGUAGE CPP #-}++module Test.Sandwich.WebDriver.Video.Internal (+  getVideoArgs+  , videoExtension+  ) where++import Control.Monad.Catch (MonadMask)+import Control.Monad.IO.Unlift+import Control.Monad.Logger+import Control.Monad.Reader+import Data.String.Interpolate+import System.FilePath+import System.Process+import Test.Sandwich+import Test.Sandwich.WebDriver.Internal.Binaries.Ffmpeg+import Test.Sandwich.WebDriver.Internal.OnDemand+import Test.Sandwich.WebDriver.Internal.Types+import Test.Sandwich.WebDriver.Types+import Test.Sandwich.WebDriver.Video.Types++#ifdef darwin_HOST_OS+getMacScreenNumber :: IO (Maybe Int)+getMacScreenNumber = return $ Just 0 -- TODO+#endif++#ifdef linux_HOST_OS+import Data.Function ((&), on)+import qualified Data.List as L+import Data.Maybe+import UnliftIO.Environment+#endif+++videoExtension :: String+videoExtension = "avi"++getVideoArgs :: (+  MonadUnliftIO m, MonadLoggerIO m, MonadMask m+  , MonadReader context m, HasBaseContext context, HasWebDriverContext context+  ) => FilePath -> (Word, Word, Int, Int) -> VideoSettings -> Maybe XvfbSession -> m (CreateProcess, FilePath)+getVideoArgs path (width, height, x, y) (VideoSettings {..}) maybeXvfbSession = do+  WebDriver {wdFfmpeg, wdFfmpegToUse} <- getContext webdriver+  ffmpeg <- getOnDemand wdFfmpeg (obtainFfmpeg wdFfmpegToUse)++#ifdef linux_HOST_OS+  displayNum <- case maybeXvfbSession of+    Nothing -> fromMaybe "" <$> (liftIO $ lookupEnv "DISPLAY")+    Just (XvfbSession {xvfbDisplayNum}) -> return $ ":" <> show xvfbDisplayNum++  baseEnv <- getEnvironment++  let env = case maybeXvfbSession of+        Nothing -> baseEnv+        Just (XvfbSession {..}) -> baseEnv+          & (("DISPLAY", displayNum) :)+          & (("XAUTHORITY", xvfbXauthority) :)+          & L.nubBy ((==) `on` fst)++  let videoPath = path <.> videoExtension++  let cmd = ["-y"+            , "-nostdin"+            , "-f", "x11grab"+            , "-s", [i|#{width}x#{height}|]+            , "-i", [i|#{displayNum}.0+#{x},#{y}|]]+            ++ xcbgrabOptions+            ++ [videoPath]+  return ((proc ffmpeg cmd) { env = Just env }, videoPath)+#endif++#ifdef darwin_HOST_OS+  maybeScreenNumber <- liftIO getMacScreenNumber+  let videoPath = [i|#{path}.avi|]+  let cmd = case maybeScreenNumber of+        Just screenNumber -> ["-y"+                             , "-nostdin"+                             , "-f", "avfoundation"+                             , "-video-size", [i|#{width}x#{height}|]+                             , "-vf", [i|crop=#{width}:#{height}:#{x}:#{y}|]+                             , "-i", [i|#{screenNumber}|]]+                             ++ avfoundationOptions+                             ++ [videoPath]+        Nothing -> error [i|Not launching ffmpeg since OS X screen number couldn't be determined.|]+  return ((proc ffmpeg cmd) { env = Nothing }, videoPath)+#endif++#ifdef mingw32_HOST_OS+  let videoPath = [i|#{path}.mkv|]+  let cmd = ["-f", "gdigrab"+            , "-nostdin"+            , "-i", "desktop"+            , "-offset_x", [i|#{x}|]+            , "-offset_y", [i|#{y}|]+            , "-video-size", [i|#{width}x#{height}|]+            ]+            ++ gdigrabOptions+            ++ [videoPath]+  return ((proc ffmpeg cmd) { env = Nothing }, videoPath)+#endif
+ src/Test/Sandwich/WebDriver/Video/Types.hs view
@@ -0,0 +1,56 @@++module Test.Sandwich.WebDriver.Video.Types where+++-- | Default options for fast X11 video recording.+fastX11VideoOptions :: [String]+fastX11VideoOptions = ["-an"+                      , "-r", "30"+                      , "-vcodec"+                      , "libxvid"+                      , "-qscale:v", "1"+                      , "-threads", "0"]++-- | Default options for quality X11 video recording.+qualityX11VideoOptions :: [String]+qualityX11VideoOptions = ["-an"+                         , "-r", "30"+                         , "-vcodec", "libx264"+                         , "-preset", "veryslow"+                         , "-crf", "0"+                         , "-threads", "0"]++-- | Default options for AVFoundation recording (for Darwin).+defaultAvfoundationOptions :: [String]+defaultAvfoundationOptions = ["-r", "30"+                             , "-an"+                             , "-vcodec", "libxvid"+                             , "-qscale:v", "1"+                             , "-threads", "0"]++-- | Default options for gdigrab recording (for Windows).+defaultGdigrabOptions :: [String]+defaultGdigrabOptions = ["-framerate", "30"]++data VideoSettings = VideoSettings {+  xcbgrabOptions :: [String]+  -- ^ Arguments to x11grab, used with Linux.+  , avfoundationOptions :: [String]+  -- ^ Arguments to avfoundation, used with OS X.+  , gdigrabOptions :: [String]+  -- ^ Arguments to gdigrab, used with Windows.+  , hideMouseWhenRecording :: Bool+  -- ^ Hide the mouse while recording video. Linux and Windows only.+  , logToDisk :: Bool+  -- ^ Log ffmpeg stdout and stderr to disk.+  }++-- | Default video settings.+defaultVideoSettings :: VideoSettings+defaultVideoSettings = VideoSettings {+  xcbgrabOptions = fastX11VideoOptions+  , avfoundationOptions = defaultAvfoundationOptions+  , gdigrabOptions = defaultGdigrabOptions+  , hideMouseWhenRecording = False+  , logToDisk = True+  }
src/Test/Sandwich/WebDriver/Windows.hs view
@@ -1,66 +1,86 @@ -- | Functions for manipulating browser windows. - module Test.Sandwich.WebDriver.Windows (   -- * Window positioning   setWindowLeftSide   , setWindowRightSide   , setWindowFullScreen -  -- * Querying screen info+  -- * Screen resolution   , getScreenResolution++  -- * Lower-level+  , getResolution+  , getResolutionForDisplay   ) where -import Control.Exception.Safe import Control.Monad.IO.Class-import Control.Monad.Logger (MonadLogger) import Control.Monad.Reader-import Data.Bits as B import Data.Maybe-import GHC.Stack import Test.Sandwich import Test.Sandwich.WebDriver.Internal.Types import Test.Sandwich.WebDriver.Resolution+import Test.Sandwich.WebDriver.Types import Test.WebDriver import qualified Test.WebDriver.Class as W   -- | Position the window on the left 50% of the screen.-setWindowLeftSide :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, W.WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()+setWindowLeftSide :: (WebDriverMonad m context, MonadReader context m, W.WebDriver m) => m () setWindowLeftSide = do   sess <- getContext webdriver   (x, y, width, height) <- case runMode $ wdOptions sess of     RunHeadless (HeadlessConfig {..}) -> return (0, 0, w, h)       where (w, h) = fromMaybe (1920, 1080) headlessResolution     _ -> getScreenResolution sess++  (screenWidth, screenHeight) <- getScreenPixelDimensions width height+   setWindowPos (x, y)-  setWindowSize (fromIntegral $ B.shift width (-1), fromIntegral height)+  setWindowSize (round (screenWidth / 2.0), round screenHeight)  -- | Position the window on the right 50% of the screen.-setWindowRightSide :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, W.WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()+setWindowRightSide :: (WebDriverMonad m context, MonadReader context m, W.WebDriver m) => m () setWindowRightSide = do   sess <- getContext webdriver   (x, y, width, height) <- case runMode $ wdOptions sess of     RunHeadless (HeadlessConfig {..}) -> return (0, 0, w, h)       where (w, h) = fromMaybe (1920, 1080) headlessResolution     _ -> getScreenResolution sess-  let pos = (x + (fromIntegral $ B.shift width (-1)), y + 0)-  setWindowPos pos-  setWindowSize (fromIntegral $ B.shift width (-1), fromIntegral height) +  (screenWidth, screenHeight) <- getScreenPixelDimensions width height++  setWindowPos (x + round (screenWidth / 2.0), y + 0)+  setWindowSize (round (screenWidth / 2.0), round screenHeight)+ -- | Fullscreen the browser window.-setWindowFullScreen :: (HasCallStack, MonadIO wd, WebDriverContext context wd, MonadReader context wd, W.WebDriver wd, MonadLogger wd, MonadMask wd) => wd ()+setWindowFullScreen :: (WebDriverMonad m context, MonadReader context m, W.WebDriver m) => m () setWindowFullScreen = do   sess <- getContext webdriver   (x, y, width, height) <- case runMode $ wdOptions sess of     RunHeadless (HeadlessConfig {..}) -> return (0, 0, w, h)       where (w, h) = fromMaybe (1920, 1080) headlessResolution     _ -> getScreenResolution sess++  (screenWidth, screenHeight) <- getScreenPixelDimensions width height+   setWindowPos (x + 0, y + 0)-  setWindowSize (fromIntegral width, fromIntegral height)+  setWindowSize (round screenWidth, round screenHeight)  -- | Get the screen resolution as (x, y, width, height). (The x and y coordinates may be nonzero in multi-monitor setups.)-getScreenResolution :: (HasCallStack, MonadIO m, MonadMask m, MonadLogger m) => WebDriver -> m (Int, Int, Int, Int)-getScreenResolution (WebDriver {wdWebDriver=(_, _, _, _, _, maybeXvfbSession)}) = case maybeXvfbSession of+-- This function works with both normal 'RunMode' and Xvfb mode.+getScreenResolution :: (MonadIO m) => WebDriver -> m (Int, Int, Int, Int)+getScreenResolution (WebDriver {wdWebDriver=(_, maybeXvfbSession)}) = case maybeXvfbSession of   Nothing -> liftIO getResolution   Just (XvfbSession {..}) -> liftIO $ getResolutionForDisplay xvfbDisplayNum++getScreenPixelDimensions :: (MonadIO m, W.WebDriver m) => Int -> Int -> m (Double, Double)+getScreenPixelDimensions width height = do+  devicePixelRatio <- executeJS [] "return window.devicePixelRatio" >>= \case+    Just (ratio :: Double) -> pure ratio+    Nothing -> pure 1.0++  let screenWidth = fromIntegral width / devicePixelRatio+  let screenHeight = fromIntegral height / devicePixelRatio++  return (screenWidth, screenHeight)
test/Spec.hs view
@@ -1,6 +1,7 @@  import Test.Sandwich import Test.Sandwich.WebDriver.Config+import Test.Sandwich.WebDriver.Binaries import Data.Text as T import UnliftIO.Temporary import Data.Time.Clock@@ -9,22 +10,25 @@  spec :: TopSpec spec = do-  it "successfully runs obtainChromeDriver" $ do-    withSystemTempDirectory "test-download" $ \dir -> do-      obtainChromeDriver dir (DownloadChromeDriverAutodetect Nothing) >>= \case-        Right x -> info [i|Got chromedriver: #{x}|]-        Left err -> expectationFailure (T.unpack err)+  it "works" $ do+    2 `shouldBe` 2 -  it "successfully runs obtainGeckoDriver" $ do-    withSystemTempDirectory "test-download" $ \dir -> do-      obtainGeckoDriver dir (DownloadGeckoDriverAutodetect Nothing) >>= \case-        Right x -> info [i|Got geckoDriver: #{x}|]-        Left err -> expectationFailure (T.unpack err)+  -- it "successfully runs obtainChromeDriver" $ do+  --   withSystemTempDirectory "test-download" $ \dir -> do+  --     obtainChromeDriver dir (DownloadChromeDriverAutodetect Nothing) >>= \case+  --       Right x -> info [i|Got chromedriver: #{x}|]+  --       Left err -> expectationFailure (T.unpack err) +  -- it "successfully runs obtainGeckoDriver" $ do+  --   withSystemTempDirectory "test-download" $ \dir -> do+  --     obtainGeckoDriver dir (DownloadGeckoDriverAutodetect Nothing) >>= \case+  --       Right x -> info [i|Got geckoDriver: #{x}|]+  --       Left err -> expectationFailure (T.unpack err) + main :: IO () main = runSandwich options spec   where     options = defaultOptions {-      optionsTestArtifactsDirectory = TestArtifactsGeneratedDirectory "test_runs" (show <$> getCurrentTime)+      optionsTestArtifactsDirectory = defaultTestArtifactsDirectory       }
unix-src/Test/Sandwich/WebDriver/Internal/StartWebDriver/Xvfb.hs view
@@ -1,7 +1,7 @@-{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE CPP #-}-{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-}  module Test.Sandwich.WebDriver.Internal.StartWebDriver.Xvfb (@@ -11,8 +11,9 @@ import Control.Exception import Control.Monad.Catch (MonadMask) import Control.Monad.IO.Class+import Control.Monad.IO.Unlift import Control.Monad.Logger-import Control.Monad.Trans.Control (MonadBaseControl)+import Control.Monad.Reader import Control.Retry import qualified Data.List as L import Data.Maybe@@ -24,7 +25,10 @@ import System.IO.Temp import System.Process import Test.Sandwich+import Test.Sandwich.WebDriver.Internal.Binaries.Xvfb+import Test.Sandwich.WebDriver.Internal.OnDemand import Test.Sandwich.WebDriver.Internal.Types+import UnliftIO.MVar   #ifdef linux_HOST_OS@@ -39,11 +43,16 @@ handleToFd h = Fd <$> HFD.handleToFd h #endif -type Constraints m = (HasCallStack, MonadLogger m, MonadIO m, MonadBaseControl IO m, MonadMask m)+type Constraints m context = (+  HasCallStack, MonadLoggerIO m, MonadUnliftIO m, MonadMask m, MonadFail m+  , MonadReader context m, HasBaseContext context+  )  -makeXvfbSession :: Constraints m => Maybe (Int, Int) -> Bool -> FilePath -> m (XvfbSession, [(String, String)])-makeXvfbSession xvfbResolution xvfbStartFluxbox webdriverRoot = do+makeXvfbSession :: (+  Constraints m context+  ) => Maybe (Int, Int) -> Bool -> FilePath -> XvfbToUse -> MVar (OnDemand FilePath) -> m (XvfbSession, [(String, String)])+makeXvfbSession xvfbResolution xvfbStartFluxbox webdriverRoot xvfbOnDemand xvfbToUse = do   let (w, h) = fromMaybe (1920, 1080) xvfbResolution   liftIO $ createDirectoryIfMissing True webdriverRoot @@ -51,7 +60,7 @@   (serverNum :: Int, p, authFile, displayNum) <- recoverAll policy $ \_ -> do     withTempFile webdriverRoot "x11_server_num" $ \path tmpHandle -> do       fd <- liftIO $ handleToFd tmpHandle-      (serverNum, p, authFile) <- createXvfbSession webdriverRoot w h fd+      (serverNum, p, authFile) <- createXvfbSession webdriverRoot w h fd xvfbOnDemand xvfbToUse        debug [i|Trying to determine display number for auth file '#{authFile}', using '#{path}'|] @@ -80,18 +89,25 @@   return (xvfbSession, env)  -createXvfbSession :: Constraints m => FilePath -> Int -> Int -> Fd -> m (Int, ProcessHandle, FilePath)-createXvfbSession webdriverRoot w h (Fd fd) = do+createXvfbSession :: (+  Constraints m context+  ) => FilePath -> Int -> Int -> Fd -> XvfbToUse -> MVar (OnDemand FilePath) -> m (Int, ProcessHandle, FilePath)+createXvfbSession webdriverRoot w h (Fd fd) xvfbToUse xvfbOnDemand = do+  xvfb <- getOnDemand xvfbOnDemand (obtainXvfb xvfbToUse)+   serverNum <- liftIO findFreeServerNum    -- Start the Xvfb session   authFile <- liftIO $ writeTempFile webdriverRoot ".Xauthority" ""-  p <- createProcessWithLogging $ (proc "Xvfb" [":" <> show serverNum-                                               , "-screen", "0", [i|#{w}x#{h}x24|]-                                               , "-displayfd", [i|#{fd}|]-                                               , "-auth", authFile-                                               ]) { cwd = Just webdriverRoot-                                                  , create_group = True }+  p <- createProcessWithLogging $ (+    proc xvfb [":" <> show serverNum+              , "-screen", "0", [i|#{w}x#{h}x24|]+              , "-displayfd", [i|#{fd}|]+              , "-auth", authFile+              ]) {+      cwd = Just webdriverRoot+      , create_group = True+      }    return (serverNum, p, authFile) @@ -106,7 +122,7 @@         False -> return candidate  -startFluxBoxOnDisplay :: Constraints m => FilePath -> Int -> m ProcessHandle+startFluxBoxOnDisplay :: Constraints m context => FilePath -> Int -> m ProcessHandle startFluxBoxOnDisplay webdriverRoot x = do   logPath <- liftIO $ writeTempFile webdriverRoot "fluxbox.log" "" 
windows-src/Test/Sandwich/WebDriver/Resolution.hsc view
@@ -13,4 +13,4 @@ getResolution = undefined  getResolutionForDisplay :: Int -> IO (Int, Int, Int, Int)-getResolutionForDisplay n = undefined+getResolutionForDisplay _n = undefined