ruby-on-railsrspeccapybara

How to write an RSpec matcher that respects Capybara's within block?


I'm trying to write a custom RSpec matcher to be used in Rails system tests, running under Capybara — the idea is to match text while ignoring certain <span> tags with it.

This is the matcher:

RSpec::Matchers.define :have_cleaned_text do |text|
  match do |content|
    content.body.include?(text) || content.body.gsub(%r{<span class=["']separator["'].*?>.*?</span>}, ' ').include?(text)
  end
end

and the HTML body of the page being tested:

<h1>Test Page</h1>
<div id='constraint'>
  <p>Clean text</p>
  <p>Broken<span class='separator'>|<span>text</p>
</div>

The first two tests pass:

within('#constraint') do
  expect(page).to have_cleaned_text('Clean text')
  expect(page).to have_cleaned_text('Broken text')
  expect(page).not_to have_cleaned_text('Test Page') # fails!
end

…but the third fails, as have_cleaned_text is ignoring the within block and testing against the whole page.

How can I make my matcher respect the within block? I would have expected it to have been passed as content, not the whole page…


Solution

  • In your example page is a Capybara session (which contains its current scope). When you call body (source and html are aliases) on a session it returns the HTML source of the document. Since you're looking for the HTML source of an element you need something like

    RSpec::Matchers.define :have_cleaned_text do |text|
      match do |session|
        session.current_scope[:innerHTML].include?(text) || session.current_scope[:innerHTML].gsub(%r{<span class=["']separator["'].*?>.*?</span>}, ' ').include?(text)
      end
    end
    

    Note that a matcher written like that won't have any waiting/retrying behavior, like the Capybara provided matchers, so you need to ensure your page is loaded/stable before using it.