Thursday, May 03, 2012

Some selenium tips

UPDATES
  • May 31, 2012: Added the tip to capture the screen when test fails.
  • May 11, 2012: Added the tip that about keeping the browser window displaying (or hidden) during the tests.
  • May 11, 2012: Rephrase the scroll to a button before clicking it tip.

Watch out the outdated articles on the internet.

Selenium 2.0 is completely different from Selenium 1.x. Selenium 2.0 is also called the selenium webdriver. So always add the keyword webdriver when googling for answers to your selenium related questions.

Implement the web UI in a modular way so it's more selenium testable.

Modularize your view logic so that you only update the part of DOM that is needed to change when your models change. If you tend to re-create a bigger part of the DOM than necessary, it's not only a waste but also could introduce risk to your functional tests written in Selenium.

Reduce unnecessary dependency on DOM structure, make element locating logic as simple as possible.

When you need to locate an element, try not rely on the DOM structure too much - for example, using code logic to locate element is the most risky one. The best approach is probably to always use a scoped CSS selector with 1 or 2 levels of IDs, And if you can locate it in one selector, don't do it in two. For example
  label = driver.find_element("#info-panel #name-label")
is more robust than
  panel = driver.find_element("#info-panel")
  label = panel.find_element("#name-label")

Do waiting in selenium the smart way.

Don't use implicit wait blindly. Implicit wait makes sense when you use find_element to find one element. But when you try to locate multiple elements by driver.find_elements, the driver will always wait the whole timeout period if implicit wait is set. That might not be what you always want. I usually write my own safe find_element method. Here is an example in the base class of my page objects:
    def s selector
      wait_until { @driver.find_element css: selector }
    end

    def wait_until(&block)
      wait = Selenium::WebDriver::Wait.new(timeout: 10, interval: INTERVAL)
      wait.until &block
    end
So that I can write the following code in my page object
   def submit_order
     s('button#submit').click
   end
The short method name "s" is inspired by jQuery. Here it will keep polling the DOM for 10 seconds until it finds the button with id "submit". It's like implicit wait but only for finding one element. When you really need to wait for multiple elements, you can use an explicit wait, which, to me, makes more sense than a hidden implicit one.

Set the initial browser window size when using Chromedriver.

Ruby code:
  profile = Selenium::WebDriver::Chrome::Profile.new
  profile['browser.window_placement.top'] = 0
  profile['browser.window_placement.left'] = 0
  profile['browser.window_placement.right'] = 1024
  profile['browser.window_placement.bottom'] = 768
  driver = Selenium::WebDriver.for :chrome, profile: profile
This works in both Windows and OSX (will try Linux and update here)
Bad news for Java, C# and Python coders though, it seems that as of now setting chrome preference is not supported in the java version of Webdrive. Your best chance could be creating a ChromeProfile class based on the exiting FirefoxProfile class.

Scroll to a button before clicking it.

Clicking buttons sometimes randomly fail. It could be caused by the fact that the button has to be the view area to be clickable and somehow the selenium auto scroll failed. In this case, add a scroll to button will improve the robustness of your suite.

When running the test using Firefox, it matters whether the browser window is displayed on the screen or hidden behind other windows.

From my experience, my guess is that selenium interacts with the browser in slightly different ways depending on if the browser is displaying in the front or not. There is rare case that certain selenium operations only work when the Firefox is displaying in the front. When running the selenium suite, changing the z position of the browser window (and thus either show or hide the browser window from other applications) can affect the tests. So you get more consistent results by keeping the browser either showing in the front(or hidden in the back) during the course of full suite.

Capture screenshots when test fails (RSpec)

In your spec_helper.rb
  RSpec.configure do |config|
    config.after(:each) do
      capture_screen_when_fails(example, @page) 
    end

    def capture_screen_when_fails example, page
      if(example.exception.present? and page.present?)
        page.capture_screen(example.description) 
      end
    end
  end
Note here that you need to keep your page object instance in an instance variable(in my case @page) in your spec. I used a naive way to name the screenshot after the example's description.
Now in your base class for your page object.
    def capture_screen filename
      path = "PATH_TO_TMP/#{filename}.png"
      @driver.save_screenshot(path) 
    end
Note that @driver is the instance variable holding the selenium webdriver currently running.