Monday, October 31, 2011

Clojure and the Firefox Zombie

Since it's Halloween, I thought it might be fun to use Clojure to turn Firefox into a mindless zombie, slavishly obeying my every command. Muahahaha.

I'm too lazy to do the heavy lifting myself, so I'll just steal the functionality I need from the Selenium Web Driver. Part of the pom.xml file looks like this:


From this I can easily create a project.clj file with lein new.

(defproject salty "1.0.0-SNAPSHOT"
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.2.1"]
                 [org.seleniumhq.selenium/selenium-java "2.11.0"]])
;; Google tells me the latest version is 2.11.0, not 2.9.0

Now for a lein deps...

Awesome, all my selenium jars downloaded. I'm using Emacs with swank-clojure installed, so I can just hit M-x clojure-jack-in and start playing around. Let me see if I can do the sample project from the Selenium tutorial.

The Java code starts off like this:

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

Ok, that's easy enough:

user> (import '[org.openqa.selenium By WebDriver])
user> (import '[org.openqa.selenium WebElement])
user> (import 'org.openqa.selenium.firefox.FirefoxDriver)
user> (import '[ ExpectedCondition WebDriverWait])

Yeah, I wimped out a little--Java interop isn't my strong suit, so I did it a bit at a time in case I screwed things up. So what's next? Well, the tutorial code, stripped of all the comments and OOP stuff, looks like this:

WebDriver driver = new FirefoxDriver();
WebElement element = driver.findElement("q"));
System.out.println("Page title is: " + driver.getTitle());

According to the accompanying text, that should open up Firefox, command it to go to Google, and search for cheese. I'm not hungry, so I'll search for clojure instead.

user> (def driver (FirefoxDriver.))

Awesome, that just launched Firefox. It's sitting there waiting to do my nefarious bidding.

user> (.get driver "")

Muahaha, it opened up Google! World domination, here I come!

user> (def elem (.findElement driver (By/name "q")))
user> (.sendKeys elem "Clojure")
; Evaluation aborted.

Oops, that didn't work. Can't I just send a straight string?

user> elem
#<FirefoxWebElement org.openqa.selenium.firefox.FirefoxWebElement@a04e5eb2>

My elem looks ok, maybe it's some variation on the syntax? Or maybe I need a type hint?

user> (.sendKeys ^WebElement elem "Clojure")
; Evaluation aborted.

Interesting. Now the stack trace is complaining about "Can't convert LCharacter to LCharacterSeq" or some such. Let's try a few more variations...

user> (.sendKeys ^WebElement elem (into-array "Clojure"))
; Evaluation aborted.
user> (.sendKeys ^WebElement elem ["Clojure"])
; Evaluation aborted.
user> (.sendKeys ^WebElement elem (into-array ["Clojure"]))

Aha, that's the trick! My browser types in "Clojure" in the search box.

user> (.submit elem)

Odd, nothing happened. Once the sendKeys method executed, the search box popped up an autocomplete menu, and it's just sitting there. Wait, I know!

user> (.sendKeys ^WebElement elem (into-array ["\n"]))
user> (.submit elem)

There we go. The autocomplete menu responded to the <newline> by going away, and then the submit() function actually submitted the search form, and Google searched for Clojure for me. One last test from the tutorial:

user> (.getTitle driver)
"clojure - Google Search"

It's aliiiiive! I hope you all have a happy halloween. I know I'm going to. Muahaha.


  1. This was Linux by the way. I'd be curious to hear from anybody who happened to try this on Windows.

  2. if you made a jar that would do all this, would you need to include delays to wait for firefox to respond? or will it automatically wait until the previous step has completed?

  3. I'm not sure. I've only just scratched the surface of what Selenium Web Driver will do, and I expect I'll find out as I keep playing with it. The docs mention "implicit waits vs. explicit waits," so it looks like there's some sort of support for that, but I haven't dug into it yet.