rails / rails-dom-testing

Extracting DomAssertions and SelectorAssertions from ActionView.
MIT License
175 stars 57 forks source link

SubstitutionContext#substitute! fails with multiple patterns #75

Closed speckins closed 2 years ago

speckins commented 6 years ago

When passing multiple patterns to #assert_select, an invalid selector is built because of the question mark (?) included in the substitution, "(?-mix:pattern)".

Here's a test case. I ran it from the test/ directory of this repo with Ruby 2.5.1.

# test/bug_test.rb
require 'test_helper'
require 'rails/dom/testing/assertions/selector_assertions'

class BugTest < ActiveSupport::TestCase
  include Rails::Dom::Testing::Assertions::SelectorAssertions

  def document_root_element
    @html_document.root
  end

  def render content
    @html_document = Nokogiri::HTML::Document.parse content
  end

  # Passes
  def test_substituted_selector
    render '<input name="item-10" value="1">'
    assert_select ':match("name", "(?-mix:item-\\d+)"):match("value", "(?-mix:\\d+)")'
  end

  # Fails with:
  #   Nokogiri::CSS::SyntaxError: unexpected '(' after '["\"name\"", "\"(\""]'
  def test_multiple_patterns
    render '<input name="item-10" value="1">'
    assert_select ':match("name", ?):match("value", ?)', /item-\d+/, /\d+/
  end
end

The problem is in SubstitutionContext#substitute! when format_for_presentation is false. On the first iteration, the first question mark is replaced with a string that also contains a question mark:

"input[name=\"(?-mix:item-\\d+)\"][value=?]"

On the second iteration, instead of replacing the next question mark, the one from the replacement is itself replaced:

"input[name=\"(\"(?-mix:\\d+)\"-mix:item-\\d+)\"][value=?]"
speckins commented 6 years ago

Using String#gsub would fix the "replacing the replacement" problem, but the block form behaves differently than the replacement-as-argument form and requires an additional #sub to "unescape" the backslashes.

def substitute!(selector, values, format_for_presentation = false)
  selector.gsub @substitute do |match|
    next match[0] if values.empty? || !substitutable?(values.first)
    matcher_for(values.shift, format_for_presentation).sub '\\\\', '\\'
  end
end