rails-dom-testing 2.0.3 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0af31466148a2f967def0233933062ca454b1bcb
4
- data.tar.gz: 825ff206fc31a115d41c567614331e779ccc8d92
2
+ SHA256:
3
+ metadata.gz: d8720c7b560a8e3acaa5ae87a7dbfb587fe47c156d8f49b9d319b2c089a3d987
4
+ data.tar.gz: f482fcae81574f5fedc71d6e46ce372f8f273eac9df50a40d565696db1a02209
5
5
  SHA512:
6
- metadata.gz: 51291b7d5bf443fa4df3b9fa38ef92b84ecef5e434a77b9681f2b63bd3188aaf59d13b722e08a27e1f3a21fb1a2872d42a05a92e838c072d0059983edc4b6878
7
- data.tar.gz: af4705e0a6acb6d1009ad3cdec42ab3dfc3eedb9e4a8ec28a4b112bbd0f1e53badfd8fa853a7146644d445c876b71b89fb4dd269e875e366c5d33f5850c55767
6
+ metadata.gz: bb404e501783f50dfd614101ee141ccc08665e2410fbd3c2e62c6859e4ed2edd2097169373b86d6deaefb63192385cf895d1ae0c512c69f65de760d38a8b6542
7
+ data.tar.gz: afc3f0247a26905b8aeb37fd94a9a0bd79f09b2ed334b539129f9566d3c25aed44e0e3a6a34f73c4ad0c6c485a24ae30e205b0aac14ff7e60b942be5d32d7663
data/README.md CHANGED
@@ -2,29 +2,9 @@
2
2
 
3
3
  This gem is responsible for comparing HTML doms and asserting that DOM elements are present in Rails applications.
4
4
  Doms are compared via `assert_dom_equal` and `assert_dom_not_equal`.
5
- Elements are asserted via `assert_select`, `assert_select_encoded`, `assert_select_email` and a subset of the dom can be selected with `css_select`.
5
+ Elements are asserted via `assert_dom`, `assert_dom_encoded`, `assert_dom_email` and a subset of the dom can be selected with `css_select`.
6
6
  The gem is developed for Rails 4.2 and above, and will not work on previous versions.
7
7
 
8
- ## Nokogiri::CSS::SyntaxError exceptions when upgrading to Rails 4.2:
9
-
10
- Nokogiri is slightly stricter about the format of CSS selectors than the previous implementation.
11
-
12
- Check the 4.2 release notes [section on `assert_select`](http://edgeguides.rubyonrails.org/4_2_release_notes.html#assert-select) for help.
13
-
14
- ## Installation
15
-
16
- Add this line to your application's Gemfile:
17
-
18
- gem 'rails-dom-testing'
19
-
20
- And then execute:
21
-
22
- $ bundle
23
-
24
- Or install it yourself as:
25
-
26
- $ gem install rails-dom-testing
27
-
28
8
  ## Usage
29
9
 
30
10
  ### Dom Assertions
@@ -41,21 +21,59 @@ assert_dom_not_equal '<h1>Portuguese</h1>', '<h1>Danish</h1>'
41
21
  # implicitly selects from the document_root_element
42
22
  css_select '.hello' # => Nokogiri::XML::NodeSet of elements with hello class
43
23
 
44
- # select from a supplied node. assert_select asserts elements exist.
45
- assert_select document_root_element.at('.hello'), '.goodbye'
24
+ # select from a supplied node. assert_dom asserts elements exist.
25
+ assert_dom document_root_element.at('.hello'), '.goodbye'
46
26
 
47
27
  # elements in CDATA encoded sections can also be selected
48
- assert_select_encoded '#out-of-your-element'
28
+ assert_dom_encoded '#out-of-your-element'
49
29
 
50
30
  # assert elements within an html email exists
51
- assert_select_email '#you-got-mail'
31
+ assert_dom_email '#you-got-mail'
52
32
  ```
53
33
 
54
34
  The documentation in [selector_assertions.rb](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb) goes into a lot more detail of how selector assertions can be used.
55
35
 
36
+ ### HTML versions
37
+
38
+ By default, assertions will use Nokogiri's HTML4 parser.
39
+
40
+ If `Rails::Dom::Testing.default_html_version` is set to `:html5`, then the assertions will use
41
+ Nokogiri's HTML5 parser. (If the HTML5 parser is not available on your platform, then a
42
+ `NotImplementedError` will be raised.)
43
+
44
+ When testing in a Rails application, the parser default can also be set by setting
45
+ `Rails.application.config.dom_testing_default_html_version`.
46
+
47
+ Some assertions support an `html_version:` keyword argument which can override the default for that
48
+ assertion. For example:
49
+
50
+ ``` ruby
51
+ # compare DOMs built with the HTML5 parser
52
+ assert_dom_equal(expected, actual, html_version: :html5)
53
+
54
+ # compare DOMs built with the HTML4 parser
55
+ assert_dom_not_equal(expected, actual, html_version: :html4)
56
+ ```
57
+
58
+ Please see documentation for individual assertions for more details.
59
+
60
+ ## Installation
61
+
62
+ Add this line to your application's Gemfile:
63
+
64
+ gem 'rails-dom-testing'
65
+
66
+ And then execute:
67
+
68
+ $ bundle
69
+
70
+ Or install it yourself as:
71
+
72
+ $ gem install rails-dom-testing
73
+
56
74
  ## Read more
57
75
 
58
- Under the hood the doms are parsed with Nokogiri and you'll generally be working with these two classes:
76
+ Under the hood the doms are parsed with Nokogiri, and you'll generally be working with these two classes:
59
77
  - [`Nokogiri::XML::Node`](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node)
60
78
  - [`Nokogiri::XML::NodeSet`](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/NodeSet)
61
79
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rails
2
4
  module Dom
3
5
  module Testing
@@ -6,44 +8,107 @@ module Rails
6
8
  # \Test two HTML strings for equivalency (e.g., equal even when attributes are in another order)
7
9
  #
8
10
  # # assert that the referenced method generates the appropriate HTML string
9
- # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
10
- def assert_dom_equal(expected, actual, message = nil)
11
- expected_dom, actual_dom = fragment(expected), fragment(actual)
11
+ # assert_dom_equal(
12
+ # '<a href="http://www.example.com">Apples</a>',
13
+ # link_to("Apples", "http://www.example.com"),
14
+ # )
15
+ #
16
+ # By default, the matcher will not pay attention to whitespace in text nodes (e.g., spaces
17
+ # and newlines). If you want stricter matching with exact matching for whitespace, pass
18
+ # <tt>strict: true</tt>:
19
+ #
20
+ # # these assertions will both pass
21
+ # assert_dom_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: false
22
+ # assert_dom_not_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: true
23
+ #
24
+ # The DOMs are created using an HTML parser specified by
25
+ # Rails::Dom::Testing.default_html_version (either :html4 or :html5).
26
+ #
27
+ # When testing in a Rails application, the parser default can also be set by setting
28
+ # +Rails.application.config.dom_testing_default_html_version+.
29
+ #
30
+ # If you want to specify the HTML parser just for a particular assertion, pass
31
+ # <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
32
+ #
33
+ # assert_dom_equal expected, actual, html_version: :html5
34
+ #
35
+ def assert_dom_equal(expected, actual, message = nil, strict: false, html_version: nil)
36
+ expected_dom, actual_dom = fragment(expected, html_version: html_version), fragment(actual, html_version: html_version)
12
37
  message ||= "Expected: #{expected}\nActual: #{actual}"
13
- assert compare_doms(expected_dom, actual_dom), message
38
+ assert compare_doms(expected_dom, actual_dom, strict), message
14
39
  end
15
40
 
16
41
  # The negated form of +assert_dom_equal+.
17
42
  #
18
43
  # # assert that the referenced method does not generate the specified HTML string
19
- # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
20
- def assert_dom_not_equal(expected, actual, message = nil)
21
- expected_dom, actual_dom = fragment(expected), fragment(actual)
44
+ # assert_dom_not_equal(
45
+ # '<a href="http://www.example.com">Apples</a>',
46
+ # link_to("Oranges", "http://www.example.com"),
47
+ # )
48
+ #
49
+ # By default, the matcher will not pay attention to whitespace in text nodes (e.g., spaces
50
+ # and newlines). If you want stricter matching with exact matching for whitespace, pass
51
+ # <tt>strict: true</tt>:
52
+ #
53
+ # # these assertions will both pass
54
+ # assert_dom_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: false
55
+ # assert_dom_not_equal "<div>\nfoo\n\</div>", "<div>foo</div>", strict: true
56
+ #
57
+ # The DOMs are created using an HTML parser specified by
58
+ # Rails::Dom::Testing.default_html_version (either :html4 or :html5).
59
+ #
60
+ # When testing in a Rails application, the parser default can also be set by setting
61
+ # +Rails.application.config.dom_testing_default_html_version+.
62
+ #
63
+ # If you want to specify the HTML parser just for a particular assertion, pass
64
+ # <tt>html_version: :html4</tt> or <tt>html_version: :html5</tt> keyword arguments:
65
+ #
66
+ # assert_dom_not_equal expected, actual, html_version: :html5
67
+ #
68
+ def assert_dom_not_equal(expected, actual, message = nil, strict: false, html_version: nil)
69
+ expected_dom, actual_dom = fragment(expected, html_version: html_version), fragment(actual, html_version: html_version)
22
70
  message ||= "Expected: #{expected}\nActual: #{actual}"
23
- assert_not compare_doms(expected_dom, actual_dom), message
71
+ assert_not compare_doms(expected_dom, actual_dom, strict), message
24
72
  end
25
73
 
26
74
  protected
75
+ def compare_doms(expected, actual, strict)
76
+ expected_children = extract_children(expected, strict)
77
+ actual_children = extract_children(actual, strict)
78
+ return false unless expected_children.size == actual_children.size
27
79
 
28
- def compare_doms(expected, actual)
29
- return false unless expected.children.size == actual.children.size
30
-
31
- expected.children.each_with_index do |child, i|
32
- return false unless equal_children?(child, actual.children[i])
80
+ expected_children.each_with_index do |child, i|
81
+ return false unless equal_children?(child, actual_children[i], strict)
33
82
  end
34
83
 
35
84
  true
36
85
  end
37
86
 
38
- def equal_children?(child, other_child)
87
+ def extract_children(node, strict)
88
+ if strict
89
+ node.children
90
+ else
91
+ node.children.reject { |n| n.text? && n.text.blank? }
92
+ end
93
+ end
94
+
95
+ def equal_children?(child, other_child, strict)
39
96
  return false unless child.type == other_child.type
40
97
 
41
98
  if child.element?
42
99
  child.name == other_child.name &&
43
100
  equal_attribute_nodes?(child.attribute_nodes, other_child.attribute_nodes) &&
44
- compare_doms(child, other_child)
101
+ compare_doms(child, other_child, strict)
45
102
  else
103
+ equal_child?(child, other_child, strict)
104
+ end
105
+ end
106
+
107
+ def equal_child?(child, other_child, strict)
108
+ if strict
46
109
  child.to_s == other_child.to_s
110
+ else
111
+ child.to_s.split == other_child.to_s.split
47
112
  end
48
113
  end
49
114
 
@@ -65,9 +130,8 @@ module Rails
65
130
  end
66
131
 
67
132
  private
68
-
69
- def fragment(text)
70
- Nokogiri::HTML::DocumentFragment.parse(text)
133
+ def fragment(text, html_version: nil)
134
+ Rails::Dom::Testing.html_document_fragment(html_version: html_version).parse(text)
71
135
  end
72
136
  end
73
137
  end
@@ -1,114 +1,132 @@
1
- require 'active_support/core_ext/module/attribute_accessors'
2
- require_relative 'substitution_context'
3
-
4
- class HTMLSelector #:nodoc:
5
- attr_reader :css_selector, :tests, :message
6
-
7
- def initialize(values, previous_selection = nil, &root_fallback)
8
- @values = values
9
- @root = extract_root(previous_selection, root_fallback)
10
- extract_selectors
11
- @tests = extract_equality_tests
12
- @message = @values.shift
13
-
14
- if @values.shift
15
- raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
16
- end
17
- end
18
-
19
- def selecting_no_body? #:nodoc:
20
- # Nokogiri gives the document a body element. Which means we can't
21
- # run an assertion expecting there to not be a body.
22
- @selector == 'body' && @tests[:count] == 0
23
- end
24
-
25
- def select
26
- filter @root.css(@selector, context)
27
- end
28
-
29
- private
30
-
31
- NO_STRIP = %w{pre script style textarea}
32
-
33
- mattr_reader(:context) { SubstitutionContext.new }
34
-
35
- def filter(matches)
36
- match_with = tests[:text] || tests[:html]
37
- return matches if matches.empty? || !match_with
38
-
39
- content_mismatch = nil
40
- text_matches = tests.has_key?(:text)
41
- regex_matching = match_with.is_a?(Regexp)
42
-
43
- remaining = matches.reject do |match|
44
- # Preserve markup with to_s for html elements
45
- content = text_matches ? match.text : match.children.to_s
46
-
47
- content.strip! unless NO_STRIP.include?(match.name)
48
- content.sub!(/\A\n/, '') if text_matches && match.name == "textarea"
49
-
50
- next if regex_matching ? (content =~ match_with) : (content == match_with)
51
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, content)
52
- true
53
- end
54
-
55
- @message ||= content_mismatch if remaining.empty?
56
- Nokogiri::XML::NodeSet.new(matches.document, remaining)
57
- end
58
-
59
- def extract_root(previous_selection, root_fallback)
60
- possible_root = @values.first
61
-
62
- if possible_root == nil
63
- raise ArgumentError, 'First argument is either selector or element ' \
64
- 'to select, but nil found. Perhaps you called assert_select with ' \
65
- 'an element that does not exist?'
66
- elsif possible_root.respond_to?(:css)
67
- @values.shift # remove the root, so selector is the first argument
68
- possible_root
69
- elsif previous_selection
70
- previous_selection
71
- else
72
- root_fallback.call
73
- end
74
- end
75
-
76
- def extract_selectors
77
- selector = @values.shift
78
-
79
- unless selector.is_a? String
80
- raise ArgumentError, "Expecting a selector as the first argument"
81
- end
82
-
83
- @css_selector = context.substitute!(selector, @values.dup, true)
84
- @selector = context.substitute!(selector, @values)
85
- end
86
-
87
- def extract_equality_tests
88
- comparisons = {}
89
- case comparator = @values.shift
90
- when Hash
91
- comparisons = comparator
92
- when String, Regexp
93
- comparisons[:text] = comparator
94
- when Integer
95
- comparisons[:count] = comparator
96
- when Range
97
- comparisons[:minimum] = comparator.begin
98
- comparisons[:maximum] = comparator.end
99
- when FalseClass
100
- comparisons[:count] = 0
101
- when NilClass, TrueClass
102
- comparisons[:minimum] = 1
103
- else raise ArgumentError, "I don't understand what you're trying to match"
104
- end
105
-
106
- # By default we're looking for at least one match.
107
- if comparisons[:count]
108
- comparisons[:minimum] = comparisons[:maximum] = comparisons[:count]
109
- else
110
- comparisons[:minimum] ||= 1
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+
5
+ require_relative "substitution_context"
6
+
7
+ module Rails
8
+ module Dom
9
+ module Testing
10
+ module Assertions
11
+ module SelectorAssertions
12
+ class HTMLSelector # :nodoc:
13
+ attr_reader :css_selector, :tests, :message
14
+
15
+ include Minitest::Assertions
16
+
17
+ def initialize(values, previous_selection = nil, &root_fallback)
18
+ @values = values
19
+ @root = extract_root(previous_selection, root_fallback)
20
+ extract_selectors
21
+ @tests = extract_equality_tests
22
+ @message = @values.shift
23
+
24
+ if @message.is_a?(Hash)
25
+ raise ArgumentError, "Last argument was a Hash, which would be used for the assertion message. You probably want this to be a String, or you have the wrong type of arguments."
26
+ end
27
+
28
+ if @values.shift
29
+ raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
30
+ end
31
+ end
32
+
33
+ def selecting_no_body? # :nodoc:
34
+ # Nokogiri gives the document a body element. Which means we can't
35
+ # run an assertion expecting there to not be a body.
36
+ @selector == "body" && @tests[:count] == 0
37
+ end
38
+
39
+ def select
40
+ filter @root.css(@selector, context)
41
+ end
42
+
43
+ private
44
+ NO_STRIP = %w{pre script style textarea}
45
+
46
+ mattr_reader(:context) { SubstitutionContext.new }
47
+
48
+ def filter(matches)
49
+ match_with = tests[:text] || tests[:html]
50
+ return matches if matches.empty? || !match_with
51
+
52
+ content_mismatch = nil
53
+ text_matches = tests.has_key?(:text)
54
+ regex_matching = match_with.is_a?(Regexp)
55
+
56
+ remaining = matches.reject do |match|
57
+ # Preserve markup with to_s for html elements
58
+ content = text_matches ? match.text : match.children.to_s
59
+
60
+ content.strip! unless NO_STRIP.include?(match.name)
61
+ content.delete_prefix!("\n") if text_matches && match.name == "textarea"
62
+
63
+ next if regex_matching ? (content =~ match_with) : (content == match_with)
64
+ content_mismatch ||= diff(match_with, content)
65
+ true
66
+ end
67
+
68
+ @message ||= content_mismatch if remaining.empty?
69
+ Nokogiri::XML::NodeSet.new(matches.document, remaining)
70
+ end
71
+
72
+ def extract_root(previous_selection, root_fallback)
73
+ possible_root = @values.first
74
+
75
+ if possible_root == nil
76
+ raise ArgumentError, "First argument is either selector or element " \
77
+ "to select, but nil found. Perhaps you called assert_dom with " \
78
+ "an element that does not exist?"
79
+ elsif possible_root.respond_to?(:css)
80
+ @values.shift # remove the root, so selector is the first argument
81
+ possible_root
82
+ elsif previous_selection
83
+ previous_selection
84
+ else
85
+ root_fallback.call
86
+ end
87
+ end
88
+
89
+ def extract_selectors
90
+ selector = @values.shift
91
+
92
+ unless selector.is_a? String
93
+ raise ArgumentError, "Expecting a selector as the first argument"
94
+ end
95
+
96
+ @css_selector = context.substitute!(selector, @values.dup, true)
97
+ @selector = context.substitute!(selector, @values)
98
+ end
99
+
100
+ def extract_equality_tests
101
+ comparisons = {}
102
+ case comparator = @values.shift
103
+ when Hash
104
+ comparisons = comparator
105
+ when String, Regexp
106
+ comparisons[:text] = comparator
107
+ when Integer
108
+ comparisons[:count] = comparator
109
+ when Range
110
+ comparisons[:minimum] = comparator.begin
111
+ comparisons[:maximum] = comparator.end
112
+ when FalseClass
113
+ comparisons[:count] = 0
114
+ when NilClass, TrueClass
115
+ comparisons[:minimum] = 1
116
+ else raise ArgumentError, "I don't understand what you're trying to match"
117
+ end
118
+
119
+ # By default we're looking for at least one match.
120
+ if comparisons[:count]
121
+ comparisons[:minimum] = comparisons[:maximum] = comparisons[:count]
122
+ else
123
+ comparisons[:minimum] ||= 1
124
+ end
125
+ comparisons
126
+ end
127
+ end
128
+ end
129
+ end
111
130
  end
112
- comparisons
113
131
  end
114
132
  end
@@ -1,33 +1,44 @@
1
- class SubstitutionContext
2
- def initialize
3
- @substitute = '?'
4
- end
1
+ # frozen_string_literal: true
5
2
 
6
- def substitute!(selector, values, format_for_presentation = false)
7
- selector = selector.dup
3
+ module Rails
4
+ module Dom
5
+ module Testing
6
+ module Assertions
7
+ module SelectorAssertions
8
+ class SubstitutionContext # :nodoc:
9
+ def initialize
10
+ @substitute = "?"
11
+ end
8
12
 
9
- while !values.empty? && substitutable?(values.first) && selector.index(@substitute)
10
- selector.sub! @substitute, matcher_for(values.shift, format_for_presentation)
11
- end
13
+ def substitute!(selector, values, format_for_presentation = false)
14
+ selector.gsub @substitute do |match|
15
+ next match[0] if values.empty? || !substitutable?(values.first)
16
+ matcher_for(values.shift, format_for_presentation)
17
+ end
18
+ end
12
19
 
13
- selector
14
- end
20
+ def match(matches, attribute, matcher)
21
+ matches.find_all { |node| node[attribute] =~ Regexp.new(matcher) }
22
+ end
15
23
 
16
- def match(matches, attribute, matcher)
17
- matches.find_all { |node| node[attribute] =~ Regexp.new(matcher) }
18
- end
24
+ private
25
+ def matcher_for(value, format_for_presentation)
26
+ # Nokogiri doesn't like arbitrary values without quotes, hence inspect.
27
+ if format_for_presentation
28
+ value.inspect # Avoid to_s so Regexps aren't put in quotes.
29
+ elsif value.is_a?(Regexp)
30
+ "\"#{value}\""
31
+ else
32
+ value.to_s.inspect
33
+ end
34
+ end
19
35
 
20
- private
21
- def matcher_for(value, format_for_presentation)
22
- # Nokogiri doesn't like arbitrary values without quotes, hence inspect.
23
- if format_for_presentation
24
- value.inspect # Avoid to_s so Regexps aren't put in quotes.
25
- else
26
- value.to_s.inspect
36
+ def substitutable?(value)
37
+ [ Symbol, Numeric, String, Regexp ].any? { |type| value.is_a? type }
38
+ end
39
+ end
40
+ end
27
41
  end
28
42
  end
29
-
30
- def substitutable?(value)
31
- value.is_a?(String) || value.is_a?(Regexp)
32
- end
43
+ end
33
44
  end