Using a business readable language for browser automation

This is a simple example of how to integrate chuchu and hs-webdriver. You can find the source code on the chuchu_webdriver repo. There, you will find the source code on the root directory as well as some files on a “support” directory that were useful to me when trying to accomplish this integration.

This test is good to show how we can leverage the readability of Business Readable DSLs to do browser automation. And it may serve as two examples that are needed from the webdriver’s todo list: basic runSession usage and explicit wait usage.

For the browser server, I’m using Selenium Server Standalone which can be downloaded from the SeleniumHQ download page. I’m going to assume you already have everything installed and working.

chuchu is similar to Cucumber and uses a language similar to Gherkin; it’s used for Behaviour-Driven Development and consists of scenarios that can be tested according to some predefined specification. The feature file can be described using much of the same basic constructs like Given, When and Then.

hs-webdriver is a Selenium WebDriver client; it has the ability to start a browser session and issue commands that control the browser like openPage and getCurrentURL.

The main focus of this example is to have a feature file that defines an interaction with the browser. You can see it’s contents below:

---- yesodweb.feature
Scenario: Check Google rank 
  Given I am on the Google search page
  When I search for "yesodweb"
  And I click the first link
  Then I should see Yesod Framework's website

Next, we have to define the steps rules that will be associated with the feature file. Here’s a first (incomplete) version:

---- yesodweb.hs
defs = do
  Given "I am on the Google search page"
  When ("I search for " *> text)
  And "I click the first link"
  Then "I should see Yesod Framework's website"

Each rule receives two arguments: one is the feature step and the other is an action to be performed if the definition matches a step. About the When rule: (1) by using text from Test.Chuchu.Parser we can obtain the quoted string as a parameter that we’ll define later and (2) make sure you have a blank space between the last word “for” and the closing quote; it must match the feature exactly.

Now we have to define an action to be executed if the parser finishes correctly. The actions to be performed will be webdriver commands. For a list of commands you can check out the documentation for Test.WebDriver.Commands.

---- yesodweb.hs
defs = do
  Given "I am on the Google search page" $ \() -> do
    # webdriver commands
  When ("I search for " *> text) $ \query -> do
    # webdriver commands
  And "I click the first link" $ \() -> do
    # webdriver commands
  Then "I should see Yesod Framework's website" $ \() -> do
    # webdriver commands

Here we defined query to be the quoted argument from the feature file. It will be used as input for the Google search.

Ok, now is the time to actually be the boss and tell the browser what to do! The first rule should be simple, just open the Google page like that:

---- snippet
Given "I am on the Google search page" $ \() -> 
  openPage ""

The second rule requires us to perform a search. For that we must get the HTML input element, get the HTML button, pass the desired query and then click the button.

---- snippet
When ("I search for " *> text) $ \query -> do
  searchBox <- findElem (ByName "q")                                                 
  searchBtn <- findElem (ByName "btnG")                                             
  sendKeys query searchBox
  click searchBtn

All right, it’s searching! You can find out the names q and btnG by looking at the HTML source code on the Google search page. Actually, on the main Google search page, the button’s name is btnK, but by default if you start typing something in the input element than Google Instant will redirect you to the results page and on that page the button’s name if btnG.

The pack function converts a String value to a Data.Text value which is the type that sendKeys receives as the first argument. This is necessary because the current version of chuchu’s text function returns a String value. However, this will be changed in future releases to return a Data.Text value thus eliminating the need for conversion. I’m using chuchu 0.1.2. This is not needed anymore since chuchu-0.2 returns a Data.Text.

Now that we are at the Google results page, we must click the first link. There’s probably several ways to do this. Here’s one way: get the div container that holds all of the results and from that get the first link element.

---- snippet
And "I click the first link" $ \() -> do
  setImplicitWait 1000                                                                               
  results <- findElem (ById "rso")                                                       
  first <- findElemFrom results (ByTag "a")                                            
  click first

The first line of the action has a setImplicitWait function which is necessary because we have to make sure the entire page has loaded before trying to get something from it. The time is in milliseconds.

Moving on, we must see if the first link is the website of the Yesod Framework. We have to wait for the entire page to load again, but this time we’ll also test if we are on the expected website. There’s a few functions that can handle wait like this; they are defined in the Test.WebDriver.Commands.Wait module.

We will use waitUntil because we want to wait until something is true. For the “something is true” part, we can use an expectation function. So, we wait until the current URL is Yesod Framework’s website.

chuchu’s test will fail if an exception is raised, which will happen if the timeout time is exceeded. It would be nice to be able to use hspec-expectations, but currently webdriver only supports its own “expect” function.

---- snippet
Then "I should see Yesod Framework's website" $ \() -> do
  waitUntil 5 $ do -- timeout (seconds)
    currURL <- getCurrentURL
    expect (currURL == "")

The first argument of waitUntil is the timeout time, in seconds. Next we have some action that will be retried until it suceeds or the timeout time exceeds. This action consists of getting the current URL and defining and expectation that it should be equal to “;. This will make sure that webdriver recognizes a success (or wait for the timeout to exceed). Notice the slash in the end!

As soon as the current URL evaluates to the expected one, than the chuchu test will succeed.

That’s really nice and all, but how are we actually communicating with the browser? For that, we must open a session that will interact with Selenium Server. For this example, the default session configuration is enough so we can have something like:

---- snippet
runSession defaultSession defaultCaps

Putting it all together gives us the following code:

---- yesodweb.hs
{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative
import Control.Monad.IO.Class
import Test.Chuchu
import Test.WebDriver
import Test.WebDriver.Commands.Wait

main :: IO()
main = chuchuMain defs configSession

configSession :: WD a -> IO a   
configSession = runSession session caps where
    session = defaultSession
    caps = defaultCaps

defs :: Chuchu WD
defs = do
       "I am on the Google search page" $
       \() ->
           openPage ""
       ("I search for "*> text) $
       \query -> do
         searchBox <- findElem (ByName "q")
         searchBtn <- findElem (ByName "btnG")
         sendKeys pack query searchBox
         click searchBtn
       "I click the first link" $
       \() -> do
         setImplicitWait 1000                      -- give time for the page to load (milliseconds)
         results <- findElem (ById "rso")          -- results div container
         first <- findElemFrom results (ByTag "a") -- get first link
         click first
  Then "I should see Yesod Framework's website" $
       \() -> 
           waitUntil 5 $ do       -- timeout (seconds)
             currURL <- getCurrentURL
             expect (currURL == "")

EDIT (30/10/2012): There’s no need to use the pack function, because chuchu-0.2 already returns a Data.Text.

We have to declare the OverloadedStrings extension to allow for the rules, which are Strings, to be recognized by the parser.

To execute it, first start the Selenium Server and then run

runhaskell yesodweb.hs yesodweb.feature

I hope it’s clear how this integration can be accomplished and if you have any questions feel free to leave a comment.

I would really like to thank Felipe Lessa for all the help he gave me. Thanks a lot! =)

8 comentários sobre “Using a business readable language for browser automation


Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do

Você está comentando utilizando sua conta Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s