This post is the fifth in a series of “Getting Started with Selenium Testing” posts from Dave Haeffner, a noted expert on Selenium and automated testing, and a frequent contributor to the Sauce blog and Selenium community. This series is for those who are brand new to test automation with Selenium and a new chapter will be posted every Tuesday (eight chapters in all).
Ideally, you should be able to write your tests once and run them across all supported browsers. While this is a rosy proposition, there is some work to make this a reliable success. And sometimes there may be a hack or two involved. But the lengths you must go really depends on the browsers you care about and the functionality you're dealing with. By using high quality locators you will be well ahead of the pack, but there are still some persnickity issues to deal with. Most notably - timing. This is especially true when working with dynamic, JavaScript heavy pages (which is more the rule than the exception in a majority of applications you'll deal with). But there is a simple approach that makes up the bedrock of reliable and resilient Selenium tests -- and that's how you wait and interact with elements. The best way to accomplish this is through the use of explicit waits.
Let's step through an example that demonstrates this against a dynamic page on the-internet. The functionality is pretty simple -- there is a button. When you click it a loading bar appears for 5 seconds, then disappears, and gets replaced with the text 'Hello World!'. Let's start by looking at the markup on the page.
Start
At a glance it's simple enough to tell that there are unique id
attributes that we can use to reference the start button and finish text. Let's add a page object for Dynamic Loading.
# filename: dynamic_loading.rb
class DynamicLoading
START_BUTTON = { css: '#start button' } FINISH_TEXT = { id: 'finish' }
def initialize(driver) @driver = driver @driver.get "http://the-internet.herokuapp.com/dynamic\_loading/1" end
def start @driver.find_element(START_BUTTON).click end
def finish_text_present? wait_for { is_displayed? FINISH_TEXT } end
def is_displayed?(locator) @driver.find_element(locator).displayed? end
def wait_for(timeout = 15) Selenium::WebDriver::Wait.new(:timeout => timeout).until { yield } end
end
This approach should look familiar to you if you checked out the last write-up. The thing which is new is the wait_for
method. In it we are using a built-in Selenium wait action. This is the mechanism with which we will perform explicit waits.
It's important to set a reasonably sized default timeout for the explicit wait. But you want to be careful not to make it too high. Otherwise you run into a lot of the same timing issues you get from implicit waits. But set it too low and your tests will be brittle, forcing you to run down trivial and transient issues. In our page object when we're usingwait_for { is_displayed? FINISH_TEXT }
we are telling Selenium to to see if the finish text is displayed on the page. It will keep trying until it either returns true
or reaches fifteen seconds -- whichever comes first. If the behavior on the page takes longer than we expect (e.g., due to slow inherently slow load times), we can simply adjust this one wait time to fix the test (e.g.,wait_for(30) { is_displayed? FINISH_TEXT }
) -- rather than increase a blanket wait time (which impacts every test). And since it's dynamic, it won't always take the full amount of time to complete.
Now that we have our page object we can wire this up in a new test file. # filename: dynamic_loading_spec.rb
require 'selenium-webdriver' require_relative 'dynamic_loading'
describe 'Dynamic Loading' do
before(:each) do @driver = Selenium::WebDriver.for :firefox @dynamic_loading = DynamicLoading.new(@driver) end
after(:each) do @driver.quit end
it 'Waited for Hidden Element' do @dynamic_loading.start @dynamic_loading.finish_text_present?.should be_true end
end
When we run it (rspec dynamic_loading_page.rb
from the command-line) it should pass, rather than throwing an exception (like in the last write-up when the element wasn't present). As an aside -- an alternative approach would be to rescue the exception like this:
def is_displayed?(locator) begin @driver.find_element(locator).displayed? rescue Selenium::WebDriver::Error::NoSuchElementError false end end
This would enable you to check the negative condition for whether or not an element is displayed. And it can be used with an explicit wait as well (it won't change it's behavior).
Let's step through one more dynamic page example to see if our explicit wait approach holds up. Our second example is laid out similarly to the last one, the main difference is that it will render the final result after the progress bar completes. Here's the markup for it.
Start
In order to find the selector for the finish text element we need to inspect the page after the loading bar sequence finishes. Here's what it looks like.
Before we add our test, we need to modify our page object to accommodate visiting the different example URLs. # filename: dynamic_loading.rb
class DynamicLoading
START_BUTTON = { css: '#start button' } FINISH_TEXT = { id: 'finish' }
def initialize(driver) @driver = driver end
def visit_example(example_number) @driver.get "http://the-internet.herokuapp.com/dynamic\_loading/#{example\_number}" end
...
Now that we have that sorted, let's add a new test to reference the markup shown above (and update our existing test to use the new.visit_example
method). # filename: dynamic_loading_spec.rb
require_relative 'dynamic_loading'
describe 'Dynamic Loading' do
...
it 'Waited for Hidden Element' do @dynamic_loading.visit_example 1 @dynamic_loading.start @dynamic_loading.finish_text_present?.should be_true end
it 'Waited for Element To Render' do @dynamic_loading.visit_example 2 @dynamic_loading.start @dynamic_loading.finish_text_present?.should be_true end
end
If we run these tests (rspec dynamic_loading_spec.rb
from the command-line) then the same approach will work for both cases. Explicit waits are one of the most important concepts in testing with Selenium. Use them often.