Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions app/Main.lhs
Original file line number Diff line number Diff line change
Expand Up @@ -63,34 +63,34 @@ My First Browser Automation

Ok! You've got your WebDriver proxy (geckodriver) running in one terminal window, and ghci running in another. Let's start with a simple example to illustrate what we can do, then explain how it works. Read this code block, even if the syntax is meaningless.

> do_a_barrel_roll :: WebDriverT IO ()
> do_a_barrel_roll = do
> release_the_bats :: WebDriverT IO ()
> release_the_bats = do
> fullscreenWindow
> navigateTo "https://www.google.com"
> performActions [typeString "do a barrel roll"]
> performActions [typeString "bats"]
> performActions [press EnterKey]
> wait 5000000
> return ()
> pure ()

Without running that code -- and maybe without being proficient in Haskell -- what do you think it does?

Now let's run it. In the interpreter, type

example1

followed by (enter). You should see a Firefox window open, go fullscreen, and search Google for "do a barrel roll".
followed by (enter). You should see a Firefox window open, go fullscreen, and search Google for "bats".

`example1`, by the way, is this:

> example1 :: IO ()
> example1 = do
> execWebDriverT defaultWebDriverConfig
> (runIsolated_ defaultFirefoxCapabilities do_a_barrel_roll)
> (runIsolated_ defaultFirefoxCapabilities release_the_bats)
> return ()

Let's break down what just happened.

1. `do_a_barrel_roll` is a *WebDriver session*, expressed in the `WebDriver` DSL. It's a high-level description for a sequence of browser actions: in this case, "make the window full screen", "navigate to google.com", and so on.
1. `release_the_bats` is a *WebDriver session*, expressed in the `WebDriver` DSL. It's a high-level description for a sequence of browser actions: in this case, "make the window full screen", "navigate to google.com", and so on.
2. `runIsolated_` takes a WebDriver session and runs it in a fresh browser instance. The parameters of this instance are specified in `defaultFirefoxCapabilities`.
3. `execWebDriver` takes a WebDriver session and carries out the steps, using some options specified in `defaultWebDriverConfig`.

Expand Down Expand Up @@ -212,7 +212,7 @@ This is `example2`:

Here's what happened:

1. `what_page_is_this` is a WebDriver session, just like `do_a_barrel_roll`, this time including an assertion: that the title of some web page is "Welcome to Lycos!".
1. `what_page_is_this` is a WebDriver session, just like `release_the_bats`, this time including an assertion: that the title of some web page is "Welcome to Lycos!".
2. `runIsolated_` runs `what_page_is_this` in a fresh browser instance.
3. `debugWebDriver` works much like `execWebDriver`, except that it collects the results of any assertion statements and summarizes them (this is `result`).
4. `printSummary` takes the assertion results and prints them out all pretty like.
Expand Down
29 changes: 15 additions & 14 deletions doc/TastyDemo.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,19 @@ Define your tests
-----------------

First things first: to make a WebDriver test suite, we need some
WebDriver tests. These are just values of type `WebDriver IO ()`. (Or
more generally, `Monad eff => WebDriver eff ()`, but that's not
important for now.) Here are a few dweeby examples. It's not necessary
for the tests to start with `_test` or use snake\_case; I'm doing it
here out of habit.
WebDriver tests. These are just values of type `WebDriverT IO ()`. (Or
more generally,
`(Monad eff, Monad (t eff), MonadTrans t) => WebDriverTT t eff ()`, but
that's not important for now.) Here are a few dweeby examples. It's not
necessary for the tests to start with `_test` or use snake\_case; I'm
doing it here out of habit.

``` {.sourceCode .literate .haskell}
_test_one :: (Monad eff) => WebDriver eff ()
_test_one :: (Monad eff) => WebDriverT eff ()
_test_one = do
navigateTo "https://google.com"

_test_two :: (Monad eff) => WebDriver eff ()
_test_two :: (Monad eff) => WebDriverT eff ()
_test_two = do
navigateTo "https://yahoo.com"
assertSuccess "time travel achieved"
Expand Down Expand Up @@ -134,7 +135,7 @@ line directly. Suppose I've got geckodriver listening on port 4444 and
chromedriver on port 9515 (which they do by default). Then I'd use the
following option:

--wd-remote-ends 'geckodriver: https://localhost:4444 chromedriver: https://localhost:9515'
--wd-remote-ends 'geckodriver https://localhost:4444 chromedriver https://localhost:9515'

(Note the explicit `https` scheme; this is required.) This is fine if
you have a small number of remote ends running, but the command line
Expand Down Expand Up @@ -173,8 +174,8 @@ behavior of your webdriver tests; use `wd-tasty-demo --help` to see a
list. Most of these are pretty specialized. Other options are pretty
common. In addition to `--wd-remote-ends` and `--wd-remote-ends-config`,
there's `--wd-driver`, for specifying which driver to use, and
`--wd-response-format`, which is required when using chromedriver
because chromedriver is not fully spec compliant.
`--wd-response-format`, which was required when using old versions of
chromedriver because it was not fully spec compliant.

Example sessions
----------------
Expand All @@ -184,19 +185,19 @@ Here are some example commands for running this demo.
Run one at a time with geckodriver:

geckodriver --port 4444 > /dev/null 2> /dev/null &
wd-tasty-demo --wd-remote-ends 'geckodriver: https://localhost:4444'
wd-tasty-demo --wd-remote-ends 'geckodriver https://localhost:4444'

Run one at a time with geckodriver, but can it with all the logs:

geckodriver --port 4444 > /dev/null 2> /dev/null &
wd-tasty-demo --wd-remote-ends 'geckodriver: https://localhost:4444' --wd-verbosity silent
wd-tasty-demo --wd-remote-ends 'geckodriver https://localhost:4444' --wd-verbosity silent

Run one at a time with chromedriver:

chromedriver --port=9515 &
wd-tasty-demo --wd-driver chromedriver --wd-response-format chromedriver --wd-remote-ends 'chromedriver: https://localhost:9515'
wd-tasty-demo --wd-driver chromedriver --wd-remote-ends 'chromedriver https://localhost:9515'

Run two at a time with geckodriver:

geckodriver --port 4444 > /dev/null 2> /dev/null &
wd-tasty-demo --wd-remote-ends 'geckodriver: https://localhost:4444' --num-threads 2
wd-tasty-demo --wd-remote-ends 'geckodriver https://localhost:4444' --num-threads 2
107 changes: 55 additions & 52 deletions doc/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import Web.Api.WebDriver
import Test.Tasty.WebDriver

import Test.Tasty
import Control.Monad.Trans.Class
import qualified System.Environment as SE
import Control.Monad
import System.IO

main :: IO ()
main = return ()
Expand Down Expand Up @@ -83,14 +85,14 @@ example to illustrate what we can do, then explain how it works. Read
this code block, even if the syntax is meaningless.

``` {.sourceCode .literate .haskell}
do_a_barrel_roll :: WebDriver IO ()
do_a_barrel_roll = do
release_the_bats :: WebDriverT IO ()
release_the_bats = do
fullscreenWindow
navigateTo "https://www.google.com"
performActions [typeString "do a barrel roll"]
performActions [typeString "bats"]
performActions [press EnterKey]
wait 5000000
return ()
pure ()
```

Without running that code -- and maybe without being proficient in
Expand All @@ -101,21 +103,21 @@ Now let's run it. In the interpreter, type
example1

followed by (enter). You should see a Firefox window open, go
fullscreen, and search Google for "do a barrel roll".
fullscreen, and search Google for "bats".

`example1`, by the way, is this:

``` {.sourceCode .literate .haskell}
example1 :: IO ()
example1 = do
execWebDriver defaultWebDriverConfig
(runIsolated_ defaultFirefoxCapabilities do_a_barrel_roll)
execWebDriverT defaultWebDriverConfig
(runIsolated_ defaultFirefoxCapabilities release_the_bats)
return ()
```

Let's break down what just happened.

1. `do_a_barrel_roll` is a *WebDriver session*, expressed in the
1. `release_the_bats` is a *WebDriver session*, expressed in the
`WebDriver` DSL. It's a high-level description for a sequence of
browser actions: in this case, "make the window full screen",
"navigate to google.com", and so on.
Expand Down Expand Up @@ -206,9 +208,10 @@ adjustments to the examples: replace
by

defaultWebDriverConfig
{ _env = defaultWDEnv
{ _remotePort = 9515
, _responseFormat = ChromeFormat
{ _environment = defaultWebDriverEnvironment
{ _env = defaultWDEnv
{ _remotePort = 9515
}
}
}

Expand All @@ -235,26 +238,27 @@ In addition to the usual browser action commands, you can sprinkle your
`WebDriver` sessions with *assertions*. Here's an example.

``` {.sourceCode .literate .haskell}
what_page_is_this :: (Monad eff) => WebDriver eff ()
what_page_is_this :: (Monad eff) => WebDriverT eff ()
what_page_is_this = do
navigateTo "https://www.google.com"
title <- getTitle
assertEqual title "Welcome to Lycos!" "Making sure we're at the lycos homepage"
return ()
```

Note the signature: `(Monad eff) => WebDriver eff ()` instead of
`WebDriver IO ()`. What's happening here is that `WebDriver` is
parameterized by the monad that effects (like writing to files and
making HTTP requests) take place in. These effects are "run" by an
explicit evaluator that, for the default configuration, happens to use
`IO`, but both the effect monad and the evaluator are configurable. By
swapping out `IO` for another type we can, for example, run our tests
against a mock Internet, and swapping out the evaluator we might have a
"dry run" evaluator that doesn't actually do anything, but logs what it
would have done. It's good practice to make our `WebDriver` code
maximally flexible by using an effect parameter like `eff` instead of
the concrete `IO` unless there's a good reason not to.
Note the signature: `(Monad eff) => WebDriverT eff ()` instead of
`WebDriverT IO ()`. What's happening here is that `WebDriverT` is a
transformer over a monad `eff` within which a restricted set of effects
(like writing to files and making HTTP requests) take place. These
effects are "run" by an explicit evaluator that, for the default
configuration, happens to use `IO`, but both the effect monad and the
evaluator are configurable. By swapping out `IO` for another type we
can, for example, run our tests against a mock Internet, and swapping
out the evaluator we might have a "dry run" evaluator that doesn't
actually do anything, but logs what it would have done. It's good
practice to make our `WebDriver` code maximally flexible by using an
effect parameter like `eff` instead of the concrete `IO` unless there's
a good reason not to.

Anyway, back to the example. What do you think this code does? Let's try
it: type
Expand All @@ -273,7 +277,7 @@ This is `example2`:
``` {.sourceCode .literate .haskell}
example2 :: IO ()
example2 = do
(_, result) <- debugWebDriver defaultWebDriverConfig
(_, result) <- debugWebDriverT defaultWebDriverConfig
(runIsolated_ defaultFirefoxCapabilities what_page_is_this)
printSummary result
return ()
Expand All @@ -282,7 +286,7 @@ example2 = do
Here's what happened:

1. `what_page_is_this` is a WebDriver session, just like
`do_a_barrel_roll`, this time including an assertion: that the title
`release_the_bats`, this time including an assertion: that the title
of some web page is "Welcome to Lycos!".
2. `runIsolated_` runs `what_page_is_this` in a fresh browser instance.
3. `debugWebDriver` works much like `execWebDriver`, except that it
Expand All @@ -308,7 +312,7 @@ Suppose we've got two WebDriver tests. These are pretty dweeby just for
illustration's sake.

``` {.sourceCode .literate .haskell}
back_button :: (Monad eff) => WebDriver eff ()
back_button :: (Monad eff) => WebDriverT eff ()
back_button = do
navigateTo "https://www.google.com"
navigateTo "https://wordpress.com"
Expand All @@ -317,7 +321,7 @@ back_button = do
assertEqual title "Google" "Behavior of 'back' button from WordPress homepage"
return ()

refresh_page :: (Monad eff) => WebDriver eff ()
refresh_page :: (Monad eff) => WebDriverT eff ()
refresh_page = do
navigateTo "https://www.mozilla.org"
pageRefresh
Expand Down Expand Up @@ -355,10 +359,11 @@ example3 = do

Here's what happened:

1. `test_suite` is a Tasty tree of individual `WebDriver` test cases.
1. `test_suite` is a Tasty tree of individual `WebDriverT` test cases.
2. `defaultWebDriverMain` is a Tasty function that runs test trees. In
this case we've also used `localOption` to tweak how the tests run
-- suppressing the usual session log output.
-- in this case suppressing the usual session log output and running
the browser in private mode.

Tasty gave us lots of nice things for free, like pretty printing test
results and timings.
Expand Down Expand Up @@ -387,20 +392,19 @@ tests with QuickCheck, if you don't find that idea abominable. :)
We need more power!
-------------------

The vanilla `WebDriver` is designed to help you control a browser with
The vanilla `WebDriverT` is designed to help you control a browser with
*batteries included*, but it has limitations. It can't possibly
anticipate all the different ways you might want to control your tests,
and it can't do arbitrary `IO`. But we have a powerful and very general
escape hatch: `WebDriver` is a special case of the `WebDriverT` monad
transformer.
escape hatch: the `WebDriverT` monad transformer is a special case of
the `WebDriverTT` monad transformer *transformer*.

The actual definition of `WebDriver` is

type WebDriver eff a = WebDriverT (IdentityT eff) a
type WebDriverT eff a = WebDriverTT IdentityT eff a

where `IdentityT` is the *inner monad* in transformer terms -- actually
it's an inner monad transformer, on the effect monad `eff`. By swapping
out `IdentityT` for another transformer we can add features specific to
where `IdentityT` is the *inner monad transformer*. By swapping out
`IdentityT` for another transformer we can add more features specific to
our application.

Here's a typical example. Say you're testing a site with two deployment
Expand Down Expand Up @@ -434,8 +438,8 @@ instance (Monad eff) => Applicative (ReaderT r eff) where
instance (Monad eff) => Functor (ReaderT r eff) where
fmap f x = x >>= (return . f)

liftReaderT :: (Monad eff) => eff a -> ReaderT r eff a
liftReaderT x = ReaderT $ \_ -> x
instance MonadTrans (ReaderT r) where
lift x = ReaderT $ \_ -> x

reader :: (Monad eff) => (r -> a) -> ReaderT r eff a
reader f = ReaderT $ \r -> return $ f r
Expand All @@ -458,28 +462,27 @@ env t = MyEnv
}
```

And we can augment `WebDriverT` with our reader transformer.
And we can augment `WebDriverTT` with our reader transformer.

``` {.sourceCode .literate .haskell}
type MyWebDriver eff a = WebDriverT (ReaderT MyEnv eff) a
type MyWebDriverT eff a = WebDriverTT (ReaderT MyEnv) eff a
```

Now we can build values in `MyWebDriver` using the same API as before,
using the extra features of the inner monad with `liftWebDriverT`.
using the extra features of the inner monad with `liftWebDriverTT`.

``` {.sourceCode .literate .haskell}
custom_environment :: (Monad eff) => MyWebDriver eff ()
custom_environment :: (Monad eff) => MyWebDriverT eff ()
custom_environment = do
theTier <- liftWebDriverT $ reader tier
theTier <- liftWebDriverTT $ reader tier
case theTier of
Test -> navigateTo "http://google.com"
Production -> navigateTo "http://yahoo.com"
```

To actually run sessions using our custom monad stack we need to make a
few adjustments. First, we use `execWebDriverT` instead of
`execWebDriver`. This function takes one extra argument corresponding to
`lift` for the inner transformer.
few adjustments. First, we use `execWebDriverTT` instead of
`execWebDriverT`.

Second, we need to supply a function that "runs" the inner transformer
(in this case `ReaderT eff a`) to `IO`.
Expand All @@ -495,7 +498,7 @@ Running our custom WebDriver monad is then straightforward.
example4 :: Tier -> IO ()
example4 t = do
execReaderT (env t) $
execWebDriverT defaultWebDriverConfig liftReaderT
execWebDriverTT defaultWebDriverConfig
(runIsolated_ defaultFirefoxCapabilities custom_environment)
return ()
```
Expand All @@ -506,8 +509,8 @@ Try it out with
example4 Production

We can similarly use a custom inner monad to check assertions and with
the tasty integration; there are analogous `debugWebDriverT` and
`testCaseT` functions.
the tasty integration; there are analogous `debugWebDriverTT` and
`testCaseTT` functions.

`ReaderT` is just one option for the inner monad transformer. We could
put mutable state, delimited continuations, or even another HTTP API
Expand All @@ -526,7 +529,7 @@ moving on.
Here's a simple example.

``` {.sourceCode .literate .haskell}
stop_and_smell_the_ajax :: (Monad eff) => WebDriver eff ()
stop_and_smell_the_ajax :: (Monad eff) => WebDriverT eff ()
stop_and_smell_the_ajax = do
breakpointsOn

Expand All @@ -544,7 +547,7 @@ We can run this with `example5`:
``` {.sourceCode .literate .haskell}
example5 :: IO ()
example5 = do
execWebDriver defaultWebDriverConfig
execWebDriverT defaultWebDriverConfig
(runIsolated_ defaultFirefoxCapabilities stop_and_smell_the_ajax)
return ()
```
Expand Down