How to test an autocomplete with Rails and Minitest?

29 Oct 2013 By: Greg Molnar

An autocomplete is a nice example for an ajax driven feature and I will demonstrate how to test such a features with Rails 4 and Minitest. First we will create a sample app and setup Minitest and Capybara for integration testing.

rails new rails-autocomplete-test

We need to add capybara and the poltergeist driver to our Gemfile:

group :development, :test do
  gem "capybara"
  gem 'poltergeist'
end

After we ran bundle we need make some changes to the test_helper.rb:

require "capybara/rails"

class ActionDispatch::IntegrationTest
  include Capybara::DSL
  require 'capybara/poltergeist'
  Capybara.javascript_driver = :poltergeist

  def teardown
    Capybara.current_driver = nil
  end
end
class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil
  def self.connection
    @@shared_connection || retrieve_connection
  end
end
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection

First we require the capybara/rails module than set the javascript driver to poltergeist. I am also setting the current_driver of capybara to nil after each test because I want to use poltergeist only when we are testing javascript features so the rest of the test suite can run faster. Than we make ActiveRecord to share the same connection between threads because capybara starts the browser in a different thread from the one our application uses and it wouldn't access to the same data in these threads. If you want to know more why we need do do this you can read Jose Valim's explanation on this link. Now we have a setup to test our javascript features so let's write a test for an autocomplete form field.<!--more--> We will expect a form with a search field on the root path:

require 'test_helper'

class AutocompleteTest < ActionDispatch::IntegrationTest

  test "autocomplete" do
    Capybara.current_driver = Capybara.javascript_driver
    visit "/"
    fill_in('search_keyword', with: 'Test')
  end
end

If we run the test it will fail of course:

1) Error:
AutocompleteTest#test_autocomplete:
Capybara::ElementNotFound: Unable to find field "search_keyword"
    test/integration/autocomplete_test.rb:8:in `block in <class:AutocompleteTest>'

1 tests, 0 assertions, 0 failures, 1 errors, 0 skips

Let's fix this and generate a controller with an action and add the missing form to the view. We also need to set the root of our application to that action:

# config/routes.rb
root 'welcome#index'
rails g controller Welcome index
  create  app/controllers/welcome_controller.rb
   route  get "welcome/index"
  invoke  erb
  create    app/views/welcome
  create    app/views/welcome/index.html.erb
  invoke  test_unit
  create    test/controllers/welcome_controller_test.rb
  invoke  helper
  create    app/helpers/welcome_helper.rb
  invoke    test_unit
  create      test/helpers/welcome_helper_test.rb
  invoke  assets
  invoke    coffee
  create      app/assets/javascripts/welcome.js.coffee
  invoke    scss
  create      app/assets/stylesheets/welcome.css.scss
# app/views/welcome/index.html.erb
<%= form_for :search do |f| %>
  <%= f.label :keyword %>
  <%= f.text_field :keyword %>
<% end %>

Now our test is passing. Next step is to extend our test to fill the form with something and see the autocomplete with some suggestion. Before we go any further we need to decide which javascript library to use as we need to know the markup it will use. I chose twitter's typeahead for this tutorial. We can copy the typeahead.js to the vendor/assets/javascripts folder and than we just need to require it in the application.js file:

app/assets/javascripts/application.js
...
//= require typeahead
...

This library uses the following markup:

<span class="tt-dropdown-menu">
  {{#dataset}}
    <div class="tt-dataset-{{name}}">
      {{{header}}}
      <span class="tt-suggestions">
        {{#suggestions}}
          <div class="tt-suggestion">{{{html}}}</div>
        {{/suggestions}}
      </span>
      {{{footer}}}
    </div>
  {{/dataset}}
</span>

It means we need to look for a div.tt-suggestion element after we entered the text to the field and compare the text of the element with the desired string:

require 'test_helper'

class AutocompleteTest < ActionDispatch::IntegrationTest
  test "autocomplete" do
    Capybara.current_driver = Capybara.javascript_driver
    visit "/"
    field = 'search_keyword'
    fill_in('search_keyword', with: 'Test')
    page.execute_script %Q{ $('##{field}').trigger("focus") }
    suggestion = page.find('div.tt-suggestion')
    assert_equal "Test", suggestion.text
  end
end

After we filled in the field we need to trigger the focus event. Of course this test fails in the moment so let's make it pass. First step is to setup a route and an action for the source of the suggestions:

# config/routes.rb
match 'suggestions' => 'welcome#suggestions', via: :get
# app/controller/welcome_controller.rb
def suggestions
  render json: [{name: 'Test'}]
end

We just render a hash as json for the sake of simplicity. Than we add some coffee to initialize typeahead:

$ ->
  $('#search_keyword').typeahead [
    {
      name: 'name'
      remote: {
        url: '/suggestions.json?q=%QUERY'
      }
      valueKey: 'name'
    }
  ]

Now if we run the test it will pass. I hope you learned how to test your javascript features with capybara and minitest. You can view the code of the sample application on this link.

PS: If you want to get updates from me please subscribe to my email list.
I hate spam as much as you do, so I won't send you anything else than Ruby/Rails related updates occasionally, and of course you can unsubcribe anytime.