Cucumber and Unobtrusive Javascript

Rails 3 will no longer pollute views with javascript code, which is cool. Until then we’ve gotta deal with it ourselves. I personally use jQuery and the following simple solution when I need a link that would make an AJAX call or a non-GET request.

I add “method” and “method_form” CSS classes to the links:

# AJAX
link_to 'Delete Item', item_path(item), :class => 'delete'
# Non-GET request (Creates a form and submits it)
link_to 'Delete Item', item_path(item), :class => 'delete_form'

And a JS snippet like this:

// All A tags with class 'get', 'post', 'put' or 'delete' will perform an ajax call
$('a.get').live('click', function(event) {
  event.preventDefault();
  $.get($(this).attr('href'));
}).attr('rel', 'nofollow');

['post', 'put', 'delete'].forEach(function(method) {
  $('a.' + method).live('click', function(event) {
    event.preventDefault();
    var self = $(this);
    var auth_token = typeof(AUTH_TOKEN) == "undefined" ? '' : AUTH_TOKEN;
    $.ajax({
      data    : '_method=' + method + '&authenticity_token=' + encodeURIComponent(auth_token),
      success : function() { self.trigger('targetedAjaxSuccess') },
      type    : 'POST',
      url     : this.href
    })
  }).attr('rel', 'nofollow');
});

// All A tags with class 'get_form', 'post_form', 'put_form' or 'delete_form' will create a form and submit it
['get', 'post', 'put', 'delete'].forEach(function(method) {
  $('a.' + method + '_form').live('click', function(event) {
    event.preventDefault();
    var auth_token = typeof(AUTH_TOKEN) == "undefined" ? '' : AUTH_TOKEN;
    var form = $('<form/>').attr({action: this.href, method: method == 'get' ? 'get' : 'post'})
    form.append($('<input/>').attr({type: hidden, name: '_method'}).val(method))
    form.append($('<input/>').attr({type: hidden, name: 'authenticity_token'}).val(auth_token))
    $(document.body).append(form)
    form.submit()
  }).attr('rel', 'nofollow');
});

The JS snippet finds all links with the CSS classes and either makes an AJAX call or creates a hidden form and submits it when someone clicks on them.

This works fine, but when it comes to Cucumber and Webrat, they don’t care about those classes and just make a GET request when I ask to follow such link. My first solution for fixing it was to modify the default “When I follow …” Cucumber step to find the link first, check if it has a CSS class and use a proper HTTP method if it does, like this:

When /^I follow "([^\"]*)"$/ do |link|
  link = webrat.current_scope.find_link(link)
  method = /(post|put|delete)/.match(link.element.attributes['class']).to_a.last
  link.click(:method => method)
end

Webrat proxies some methods (like click_link, visit, fill_in, etc) to webrat.current_scope. So that’s the object you need if you’d like to call other methods (like find_link in this case). The find_link method returns a Webrat::Link object that in turn has a Nokogiri element object (link.element), so that’s how you can get its HTML attributes.

Alright, it works. But wait, Webrat can actually handle inline JS that is generated by default Rails helpers, right? So changing that bit of code to use CSS classes would work for unobtrusive JS as well. And we won’t have to change the default Cucumber steps every time we update Cucumber. It appears that Webrat does it in Webrat::Link::http_method:

def http_method
  if !onclick.blank? && onclick.include?("f.submit()")
    http_method_from_js_form
  else
    :get
  end
end

If we override this method to look something like this (drop the following to your features/support/env.rb after Webrat is included):

module Webrat
  class Link

  protected

    def css_class
      Webrat::XML.attribute(@element, "class")
    end

    def http_method_from_css_class
      /(post|put|delete)/.match(css_class).to_a.last
    end

    def http_method
      http_method_from_css_class || :get
    end
  end
end

The standard Cucumber step starts working like we expect it to, using proper HTTP methods for requests.

I’m pretty sure that something like this will be added to Webrat once Rails 3 is released (cause it won’t have inline JS anymore). Until then this snippet can be used.