Search code examples
seleniumhaskellselenium-webdriverscotty

How to combine WebDriver and Scotty monads


I am beginner, so please bear with me.

I have a following code:

{-# LANGUAGE OverloadedStrings #-}

module Lib where

import           Control.Monad.IO.Class
import           Control.Monad.Trans.Class
import           Data.Monoid               ((<>))
import qualified Data.Text                 as T
import qualified Data.Text.Lazy            as TL
import           Test.WebDriver
--import           Web.Scotty
import           Web.Scotty.Trans

firefoxConfig :: WDConfig
firefoxConfig = defaultConfig

startMyBrowser :: WD a -> IO a
startMyBrowser = runSession firefoxConfig

stopMyBrowser = closeSession

someFunc :: WD String
someFunc = do
  openPage "http://maslo.cz"
  captionElem <- findElem (ByCSS "h2")
  text <- getText captionElem
  return $ T.unpack text

helloAction :: ActionT TL.Text WD ()
helloAction = do
  a <- lift someFunc
  text $ "got this for you: " <> TL.pack a

routes :: ScottyT TL.Text WD ()
routes = get "/hello" helloAction

startServer = startMyBrowser $ do
  lift $ scottyT 3000 _ routes
  stopMyBrowser

I am not sure if even those filled parts are right - it is supposed to start a Selenium session (startMyBrowser), spin up a web server (the scottyT part) and after web server stops it should end the Selenium session (stopMyBrowser).

After fiddling with types I got to that code above and it seems that I only miss one piece - the hole.

Please, if you make it working, try to explain your solution and/or add some links to more materials. I would love to understand those damned transformers.

Edit 1: Here are the errors:

  • Couldn't match type ‘t0 m0’ with ‘WD’
    Expected type: WD ()
      Actual type: t0 m0 ()
  • In a stmt of a 'do' block: lift $ scottyT 3000 _ routes
    In the second argument of ‘($)’, namely
      ‘do { lift $ scottyT 3000 _ routes;
            stopMyBrowser }’
    In the expression:
      startMyBrowser
      $ do { lift $ scottyT 3000 _ routes;
             stopMyBrowser }


  • Found hole:
      _ :: WD wai-3.2.1.1:Network.Wai.Internal.Response
           -> IO wai-3.2.1.1:Network.Wai.Internal.Response
  • In the second argument of ‘scottyT’, namely ‘_’
    In the second argument of ‘($)’, namely ‘scottyT 3000 _ routes’
    In a stmt of a 'do' block: lift $ scottyT 3000 _ routes
  • Relevant bindings include
      startServer :: IO () (bound at src/Lib.hs:37:1)

Solution

  • Kind guy ForTheFunctionGod on reddit answered it, here it is:

    There's your problem:

    lift $ scottyT 3000 _ routes
    

    Since the return type of scottyT is

    MonadIO n => n
    

    You don't need to lift it - it can fit into any monad that can perform IO actions. WD is such a monad - note its MonadIO instance. You'd only need to lift scottyT if its return type would be a simple IO.

    Type classes like MonadIO, MonadState, etc. mostly obviate the need to manually lift computations. Whereas you'd need to lift a function to type IO Int if you wanted to embed it into StateT s IO Int, you wouldn't need to lift a function of type MonadIO m => m a, because StateT s IO already is an instance of MonadIO, so m can be instantiated to it.

    As for the hole: it has to be of type

    WD Response -> IO Response
    

    The only way to make this work is to use runSession or one of its friends. I don't know Selenium that well, but, presumably, you can re-use your already opened session's id:

    runWD :: WDSession -> WD a -> IO a
    
    startServer = startMyBrowser $ do
       sessionID <- getSession
       scottyT 3000 (runWD sessionID) routes
       stopMyBrowser
    

    I haven't tried this out, but the types should check out. I hope it helps!


    It definitely did :).