Selenium and Javascript Events
Selenium is a great way to test web applications and it has Python bindings. I explained in a previous post how to set this up with coverage analysis.
However, writing tests is non-trivial, in particular it is easy enough to write tests that suffer from race conditions. Suppose you write a test that includes a check for the existence of a particular DOM element. Here is a convenient method to make doing so a one-liner. It assumes that you are within a class that has the web driver as a member and that you're using 'pytest' but you can easily adapt this for your own needs.
def assertCssSelectorExists(self, css_selector): """ Asserts that there is an element that matches the given css selector.""" # We do not actually need to do anything special here, if the # element does not exist we fill fail with a NoSuchElementException # however we wrap this up in a pytest.fail because the error message # is then a bit nicer to read. try: self.driver.find_element_by_css_selector(css_selector) except NoSuchElementException: pytest.fail("Element {0} not found!".format(css_selector))
The problem is that this test might fail if it is performed too early. If you are merely testing after loading a page, this should work, however you may be testing after some click by a user which invokes a Javascript method.
Suppose you have an application which loads a page, and then loads all comments made on that page (perhaps it is a blog engine). Now suppose you wish to allow re-loading the list of comments without re-loading the entire page. You might have an Ajax call.
As before I tend to write my Javascript in Coffeescript, so suppose I have a
Coffeescript function which is called when the user clicks on a
#refresh-comment-feed-button
button:
refresh_comments = (page_id) -> posting = $.post '/grabcomments', page_id: page_id posting.done receive_comments posting.fail (data) -> ...
So this makes an Ajax call which will call the function receive_comments
when the Ajax call returns (successfully). We write the receive_comments
as:
receive_comments = (data) -> ... code to delete current comments and replace them with those returned
Typically data
will be some JSON data, perhaps the comments associated with
the page_id
we gave as an argument to our Ajax call.
To test this you would navigate to the page in question and check that there are no comments, then open a new browser window and make two comments (or alternatively directly adding the comments to the database), followed by switching back to the first browser window and then performing the following steps:
refresh_comment_feed_css = '#refresh-comment-feed-button' self.click_element_with_css(refresh_comment_feed_css) self.check_comments([first_comment, second_comment])
Where self.check_comments
is a method that checks the particular comments
exist on the current page. This could be done by using
find_elements_by_css_selector
and then looking at the text
attributes of
each returned element.
The problem is, that the final line is likely to be run before the results of
the Ajax call invoked from the click on the #refresh-comment-feed-button
are
returned to the page.
A quick trick to get around this is to simply change the Javascript to somehow record when the Ajax results are returned and then use Selenium to wait until the relevant Javascript evaluates to true.
So we change our receive_comments
method to be:
comments_successfully_updated = 0 receive_comments = (data) -> ... code to delete current comments and replace them with those returned comments_successfully_updated += 1
Note that we only increment the counter after we have updated the page.
Now, we can update our Selenium test to be:
refresh_comment_feed_css = '#refresh-comment-feed-button' self.click_element_with_css(refresh_comment_feed_css) self.wait_for_comment_refresh_count(1) self.check_comments([first_comment, second_comment])
The 1
argument assumes that this will be the first time the comments are
updated during your test. Of course as you run down your test you can increase
this argument as required. The code for the wait_for_comment_refresh_count
is given by:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions from selenium.webdriver.common.by import By ... class MyTest(object): ... # assume that 'self.driver' is set appropriately. def wait_for_comment_refresh_count(self, count): def check_refresh_count(driver): script = 'return comments_successfully_updated;' feed_count = driver.execute_script(script) return feed_count == count WebDriverWait(self.driver, 5).until(check_refresh_count)
The key point is executing the Javascript to check the
comments_successfully_updated
variable with driver.execute_script
.
We then use a WebDriverWait
to wait for a maximum of 5 seconds until the
our condition is satisfied.
Conclusion
Updating a Javascript counter to record when Javascript events have occurred can allow your Selenium tests to synchronise, that is, wait for the correct time to check the results of a Javascript event.
This can solve problems of getting a StaleElementReferenceException
or a
NoSuchElementException
because your Selenium test is running a check on an
element too early before your page has been updated.
Comments
Comments powered by Disqus