RSpec::HTML provides a simple object interface to HTML responses from RSpec Rails request specs.
Add the gem to your Gemfile
:
gem 'rspec-html', '~> 0.3.0'
And rebuild your bundle:
$ bundle install
Require the gem in your spec_helper.rb
:
# spec/spec_helper.rb
require 'rspec/html'
Several matchers are provided to identify text and HTML elements within the DOM. These matchers can only be used with the provided object interface.
To open the current document in your operating system's default browser, call document.open
. Use this to inspect HTML content while debugging.
it 'has complex HTML' do
get '/my/path'
document.open
end
Alternatively document.html_path
writes the current document to a temporary file and returns its path.
The top-level object document
is available in all tests which reflects the current response body (e.g. in request specs).
If you need to parse HTML manually you can use the provided parse_html
helper and then access document
as normal:
let(:document) { parse_html('<html><body>hello</body></html>') }
it 'says hello' do
expect(document.body).to match_text 'hello'
end
This method can also be used for ActionMailer emails:
let(:document) { parse_html(ActionMailer::Base.deliveries.last.body.decoded) }
Changed in 0.3.0: parse_html
no longer assigns document
automatically, you must use a let
block to assign it yourself.
To navigate the DOM by a sequence of tag names use chained method calls on the document
object:
expect(document.body.div.span).to match_text 'some text'
To select an element matching certain attributes pass a hash to any of the chained methods:
expect(document.body.div(id: 'my-div').span(align: 'left')).to match_text 'some text'
Special attributes like checked
can be found using the #attributes
method:
expect(document.input(type: 'checkbox', name: 'my-name')).attributes).to include 'checked'
CSS classes are treated as a special case: to select an element matching a set of classes pass the class
parameter:
expect(document.body.div(id: 'my-div').span(class: 'my-class')).to match_text 'some text'
expect(document.body.div(id: 'my-div').span(class: 'my-class my-other-class')).to match_text 'some text'
Classes can be provided in any order, i.e. 'my-class my-other-class'
is equivalent to 'my-other-class my-class'
.
To use a simple CSS selector when no other attributes are needed, pass a string to the tag method:
expect(document.body.div('#my-id.my-class1.my-class2')).to match_text 'some text'
This is effectively shorthand for:
expect(document.body.div(id: 'my-id', class: 'my-class1 my-class2')).to match_text 'some text'
Use once
, twice
, times
, at_least
, and at_most
to match element counts:
expect(document.div('.my-class')).to match_text('some text').once
expect(document.div('.my-class')).to match_text('some other text').twice
expect(document.div('.my-class')).to match_text('some').times(3)
expect(document.div('.my-class')).to match_text('some').at_least(:twice)
expect(document.div('.my-class')).to match_text('some').at_least.times(3)
expect(document.div('.my-class')).to match_text('some text').at_most.once
expect(document.div('.my-class')).to match_text('some other text').at_most.twice
expect(document.div('.my-class')).to match_text('text').at_most.times(3)
To select an element that includes a given text string (i.e. excluding mark-up) use the text
option:
expect(document.body.div(text: 'some text').input[:value]).to eql 'some-value'
To select an attribute from an element use the hash-style interface:
expect(document.body.div.span[:class]).to match_text 'my-class'
expect(document.body.div.span['data-content']).to match_text 'my content'
To select all matching elements as an array, use #all
:
expect(document.span.all.map(&:text)).to eql ['foo', 'bar', 'baz']
To select an index from a set of matched elements use the array-style interface (the first matching element is 1
, not 0
):
expect(document.body.div[1].span[1][:class]).to match_text 'my-class'
Alternatively, use #first
, #last
or, when using ActiveSupport, #second
, #third
, etc. are also available:
expect(document.body.div.first.span.last[:class]).to match_text 'my-class'
To test if a matching element was found use the exist
matcher:
expect(document.body.div[1]).to exist
expect(document.body.div[4]).to_not exist
To test the length of matched elements use the #size
or #length
method:
expect(document.body.div.size).to eql 3
expect(document.body.div.length).to eql 3
If you need something more specific you can always use the Nokogiri #xpath
and #css
methods on any element:
expect(document.body.xpath('//span[@class="my-class"]')).to match_text 'some text'
expect(document.body.css('span.my-class')).to match_text 'some text'
To simply check that an XPath or CSS selector exists use have_xpath
and have_css
:
expect(document.body).to have_css 'html body div.myclass'
expect(document.body).to have_xpath '//html/body/div[@class="myclass"]'
Use be_checked
to test if a checkbox is checked:
expect(document.input(type: 'checkbox')).to be_checked
Changed in 0.3.0: The contain_text
matcher has been removed. Use match_text
instead.
Use the match_text
matcher to locate text within a DOM element. All mark-up elements are stripped when using this matcher.
This matcher receives either a string or a regular expression.
expect(document.body.form).to match_text 'Please enter your password'
expect(document.body.form).to match_text /Please enter your [a-z]+/
Use the contain_tag
matcher to locate DOM elements within any given element. This matcher accepts two arguments:
- The tag name of the element you want to match (e.g.
:div
); - (Optional) A hash of options. All options supported by the object interface can be used here.
Without options:
expect(document.div(class: 'my-class')).to contain_tag :span
With options:
expect(document.form(class: 'my-form')).to contain_tag :input, name: 'email', class: 'email-input'
Feel free to make a pull request.