Monday, May 28, 2012

System Testing Bookmarklets: Selenium

My switch search engine bookmarklet had two bugs. First when I deployed the page, the URL was screwed up (thanks for telling me Chris) and second my bookmarklet was incompatible with FireFox[1].  

Conveniently the software engineering community has a solution to this class of problems and it's called  system testing. In this context, System testing means interact with the web pages via a browser just like users do.  I did some digging and I couldn't find a perfect (or awesome) tool for system testing web apps.

I was able to get a tool that could drive FireFox and Chrome (IE isn't working).  This tool had some warts, and took some experimentation to get working, but it mostly meets the needs. Please comment if you find a better tool!

The tool I used is called Selenium, and it's scriptable via python (also Java and C#). Selenium is able to impersonate a user performing actions via the common web browsers. The code below should make reasonable sense to someone familiar with the DOM. As usual, the code is also available on bitbucket:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException
import unittest, time, re
import urllib

class RunTheClick(unittest.TestCase):
    def setUp(self):
        self.base_url = "http://ig2600.blogspot.com"
        self.drivers=[]
        
    def test_chrome(self):
        d = webdriver.Chrome()
        self.drivers.append(d)
        self.run_suite(d)

    def test_firefox(self):
        d = webdriver.Firefox()
        self.drivers.append(d)
        self.run_suite(d)

    def run_suite(self,driverToUse):
        self.driver = driverToUse
        driver = self.driver

        # Get Blog Page
        driver.get(self.base_url + "/2012/05/assembly-to-javascript-tsrs-to.html")


        bookmarklet = driver.find_element_by_partial_link_text("SwapSearchEngine")

        #bookmarkletURL starts with uri scheme(javascript:), remove it to get code
        bookmarkletURL = bookmarklet.get_attribute("href")
        bookmarkletCode = bookmarkletURL.split("javascript:")[1]

        # BUGBUG: bookmarkletCode is URLEncoded in FF but not in Chrome.
        # In future, unquoting might not be safe in chrome, find correct way to handle this.
        bookmarkletCode = urllib.unquote(bookmarkletCode)

        # click on bookmarklet in page and verify it throws a failure alert.
        bookmarklet.click()
        self.verify_and_dismess_not_on_google_or_bing_alert()

        # Selenium can't execute bookmarklet if there is an alert in play - that's too bad.
        # otherwise we'd do the below
        # driver.execute_async_script(bookmarkletCode)
        # self.verify_and_dismess_not_on_google_or_bing_alert()

        #go to google and search
        driver.get("http://google.com") 

        # Very brittle (found it using web inspect in firefox developer tools)
        textBox =  self.driver.find_element_by_id("gbqfq")
        textBox.send_keys ("a b c")
        textBox.send_keys (Keys.RETURN)
        time.sleep(2) # sleep to let autocomplete finish.
        self.assertEqual(driver.title,u"a b c - Google Search")
        
        # execute bookmarklet
        print bookmarkletCode
        driver.execute_script(bookmarkletCode);

        # verify we switched to bing
        self.assertEqual(driver.title,u"a b c - Bing")

        # execute bookmarklet
        driver.execute_script(bookmarkletCode);

        # verify back on google.
        self.assertEqual(driver.title,u"a b c - Google Search")
    
    def verify_and_dismess_not_on_google_or_bing_alert(self):
        # Click on the alert
        alert = self.driver.switch_to_alert()
        self.assertEqual(alert.text,u"Only works on a Google or Bing search")
        alert.accept()
        time.sleep(2) # sleep to let dialog go away.


    def tearDown(self):
        for d in self.drivers: 
            d.quit()

if __name__ == "__main__":
    unittest.main()
Notes:
[1] InnerText isn't support in FF, need to use InnerHtml instead.


No comments: