page_match

RSpec 2 matcher class for building custom Capybara matchers for use in acceptance-level tests.

Install

page_match is distributed as a Ruby gem:

sudo gem install page_match

Using page_match

With the rise in popularity of RSpec + Capybara as an acceptance (aka. request-level) test replacement for Cucumber, using custom matchers can go a long way to clean up your examples, as well as improve your test readability.

Consider the following HTML, which is rendered as the home page of a standard Rack-based web app:

<html>
  <head><title>Home Page</title></head>
  <body>
    <div id="#logout-link">
      <a href="/logout">Logout Joe User</a>
    </div>
    <h1 id="#main-header">Welcome Joe User</h1>
  </body>
</html>

One fairly common acceptance test would be to verify the text rendered for the logout link and the main header to make sure they include the correct user's name. In vanilla RSpec that might look like this:

describe "Visiting the home page", :type => :request do
  before(:each) do
    @user = User.(:name => "Joe User")
  end

  context "The home page" do
    before(:each) do
      visit(root_path)
    end

    it "should have the correct logout link" do
      within("#logout-link") do
        page.should have_content("Logout #{@user.name}")
      end
    end

    it "should have the correct main header" do
      within("#main-header") do
        page.should have_content("Welcome #{@user.name}")
      end
    end
  end
end

Running these examples with the --format documentation option turned on, gives us:

Visiting the home page
  The home page
    should have the correct logout link
    should have the correct main header

On the surface, this example code and the documentation output is just fine. However the code is both hard to refactor and extremely verbose, with the HTML structure traversal code directly in the examples. In addition, the output lines that describe the examples are very generic.

Using page_match we can create helper methods that wrap the matcher logic for our examples, giving us methods that we can reuse and letting us construct much better example descriptions:

module MatchHelpers
  def have_logout_link_for(name)
    PageMatch.match do |pm|
      pm.have %(a logout link for "#{name}")
      pm.page { within ("#logout-link") { has_content?("Logout #{name}") } }
    end
  end

  def have_main_header_for(name)
    PageMatch.match do |pm|
      pm.have %(a main header for "#{name}")
      pm.page { within ("#main-header") { has_content?("Welcome #{name}") } }
    end
  end
end

With these helper methods in place, we can now rewrite the acceptance test:

describe "Visiting the home page", :type => :request do
  before(:each) do
    @user = User.(:name => "Joe User")
  end

  subject { page }

  context "The home page" do
    before(:each) do
      visit(root_path)
    end

    it { should have_logout_link_for(@user.name) }
    it { should have_main_header_for(@user.name) }
  end
end

When we run this example file, with the --format documentation option, we get:

Visiting the home page
  The home page
    should have a logout link for "Joe User"
    should have a main header for "Joe User"

The resulting test file is now easier to read, contains examples that make use of custom, domain specific matchers, that when output provide us with context-specific descriptions for those examples.

Included Helper Methods

When you install page_match you will also get a set of included helper methods, for the most common types of rendered page inspections. These include:

  it { should have_link(<Link Label>) }

have_button

  it { should have_button(<Button Label>) }

have_flash_notice

  it { should have_flash_notice(<Flash Text>) }

have_form_error

  it { should have_form_error(<Error Text>) }

have_text_field

  it { should have_text_field(:form_name, :field_name) }

have_text_area

  it { should have_text_area(:form_name, :field_name) }

have_check_box

  it { should have_check_box(:form_name, :field_name) }

have_select_field

  it { should have_select_field(:form_name, :field_name) }