rails-dom-testing 2.0.3 → 2.3.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: 92842b3fcd4006daf9f8047da3d90a171ead6f39900c2d4f491d3c3ac0658845
4
+ data.tar.gz: 67b13526065e8c54acc109945f9566947f1976c601e44bb2295eaab79d7fcfe4
5
5
  SHA512:
6
- metadata.gz: 51291b7d5bf443fa4df3b9fa38ef92b84ecef5e434a77b9681f2b63bd3188aaf59d13b722e08a27e1f3a21fb1a2872d42a05a92e838c072d0059983edc4b6878
7
- data.tar.gz: af4705e0a6acb6d1009ad3cdec42ab3dfc3eedb9e4a8ec28a4b112bbd0f1e53badfd8fa853a7146644d445c876b71b89fb4dd269e875e366c5d33f5850c55767
6
+ metadata.gz: d16235fedf30e46ccc6f0fc4463f567917d12084d6d8a0b67cfce316ae7cdd5100b78d84f314e856ff56904e4ffa628628e0c2b5fee11cf1b287ddffdb04431c
7
+ data.tar.gz: 93e996ac52d49ead86b8a04ae43299303c3c02231adc0911158142e7defb49e5afc3b4584f66bb437e9983d910fc70f8bacdddfb79f648c8f1f27cd003532b87
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,62 @@ 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'
26
+
27
+ # select from a supplied node. assert_not_dom asserts elements do not exist.
28
+ assert_not_dom document_root_element.at('.hello'), '.goodbye'
46
29
 
47
30
  # elements in CDATA encoded sections can also be selected
48
- assert_select_encoded '#out-of-your-element'
31
+ assert_dom_encoded '#out-of-your-element'
49
32
 
50
33
  # assert elements within an html email exists
51
- assert_select_email '#you-got-mail'
34
+ assert_dom_email '#you-got-mail'
52
35
  ```
53
36
 
54
37
  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
38
 
39
+ ### HTML versions
40
+
41
+ By default, assertions will use Nokogiri's HTML4 parser.
42
+
43
+ If `Rails::Dom::Testing.default_html_version` is set to `:html5`, then the assertions will use
44
+ Nokogiri's HTML5 parser. (If the HTML5 parser is not available on your platform, then a
45
+ `NotImplementedError` will be raised.)
46
+
47
+ When testing in a Rails application, the parser default can also be set by setting
48
+ `Rails.application.config.dom_testing_default_html_version`.
49
+
50
+ Some assertions support an `html_version:` keyword argument which can override the default for that
51
+ assertion. For example:
52
+
53
+ ``` ruby
54
+ # compare DOMs built with the HTML5 parser
55
+ assert_dom_equal(expected, actual, html_version: :html5)
56
+
57
+ # compare DOMs built with the HTML4 parser
58
+ assert_dom_not_equal(expected, actual, html_version: :html4)
59
+ ```
60
+
61
+ Please see documentation for individual assertions for more details.
62
+
63
+ ## Installation
64
+
65
+ Add this line to your application's Gemfile:
66
+
67
+ gem 'rails-dom-testing'
68
+
69
+ And then execute:
70
+
71
+ $ bundle
72
+
73
+ Or install it yourself as:
74
+
75
+ $ gem install rails-dom-testing
76
+
56
77
  ## Read more
57
78
 
58
- Under the hood the doms are parsed with Nokogiri and you'll generally be working with these two classes:
79
+ Under the hood the doms are parsed with Nokogiri, and you'll generally be working with these two classes:
59
80
  - [`Nokogiri::XML::Node`](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node)
60
81
  - [`Nokogiri::XML::NodeSet`](http://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/NodeSet)
61
82
 
@@ -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,108 @@ 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
73
+ alias_method :refute_dom_equal, :assert_dom_not_equal
25
74
 
26
75
  protected
76
+ def compare_doms(expected, actual, strict)
77
+ expected_children = extract_children(expected, strict)
78
+ actual_children = extract_children(actual, strict)
79
+ return false unless expected_children.size == actual_children.size
27
80
 
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])
81
+ expected_children.each_with_index do |child, i|
82
+ return false unless equal_children?(child, actual_children[i], strict)
33
83
  end
34
84
 
35
85
  true
36
86
  end
37
87
 
38
- def equal_children?(child, other_child)
88
+ def extract_children(node, strict)
89
+ if strict
90
+ node.children
91
+ else
92
+ node.children.reject { |n| n.text? && n.text.blank? }
93
+ end
94
+ end
95
+
96
+ def equal_children?(child, other_child, strict)
39
97
  return false unless child.type == other_child.type
40
98
 
41
99
  if child.element?
42
100
  child.name == other_child.name &&
43
101
  equal_attribute_nodes?(child.attribute_nodes, other_child.attribute_nodes) &&
44
- compare_doms(child, other_child)
102
+ compare_doms(child, other_child, strict)
45
103
  else
104
+ equal_child?(child, other_child, strict)
105
+ end
106
+ end
107
+
108
+ def equal_child?(child, other_child, strict)
109
+ if strict
46
110
  child.to_s == other_child.to_s
111
+ else
112
+ child.to_s.split == other_child.to_s.split
47
113
  end
48
114
  end
49
115
 
@@ -65,9 +131,8 @@ module Rails
65
131
  end
66
132
 
67
133
  private
68
-
69
- def fragment(text)
70
- Nokogiri::HTML::DocumentFragment.parse(text)
134
+ def fragment(text, html_version: nil)
135
+ Rails::Dom::Testing.html_document_fragment(html_version: html_version).parse(text)
71
136
  end
72
137
  end
73
138
  end
@@ -1,114 +1,154 @@
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, refute: false, &root_fallback)
18
+ @values = values
19
+ @root = extract_root(previous_selection, root_fallback)
20
+ extract_selectors
21
+ @tests = extract_equality_tests(refute)
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
+ html_matches = tests.has_key?(:html)
55
+ regex_matching = match_with.is_a?(Regexp)
56
+
57
+ remaining = matches.reject do |match|
58
+ # Preserve markup with to_s for html elements
59
+ content = text_matches ? match.text : match.inner_html
60
+
61
+ content.strip! unless NO_STRIP.include?(match.name)
62
+ content.delete_prefix!("\n") if text_matches && match.name == "textarea"
63
+ collapse_html_whitespace!(content) unless NO_STRIP.include?(match.name) || html_matches
64
+
65
+ next if regex_matching ? (content =~ match_with) : (content == match_with)
66
+ content_mismatch ||= diff(match_with, content)
67
+ true
68
+ end
69
+
70
+ @message ||= content_mismatch if remaining.empty?
71
+ Nokogiri::XML::NodeSet.new(matches.document, remaining)
72
+ end
73
+
74
+ def extract_root(previous_selection, root_fallback)
75
+ possible_root = @values.first
76
+
77
+ if possible_root == nil
78
+ raise ArgumentError, "First argument is either selector or element " \
79
+ "to select, but nil found. Perhaps you called assert_dom with " \
80
+ "an element that does not exist?"
81
+ elsif possible_root.respond_to?(:css)
82
+ @values.shift # remove the root, so selector is the first argument
83
+ possible_root
84
+ elsif previous_selection
85
+ previous_selection
86
+ else
87
+ root_fallback.call
88
+ end
89
+ end
90
+
91
+ def extract_selectors
92
+ selector = @values.shift
93
+
94
+ unless selector.is_a? String
95
+ raise ArgumentError, "Expecting a selector as the first argument"
96
+ end
97
+
98
+ @css_selector = context.substitute!(selector, @values.dup, true)
99
+ @selector = context.substitute!(selector, @values)
100
+ end
101
+
102
+ def extract_equality_tests(refute)
103
+ comparisons = {}
104
+ case comparator = @values.shift
105
+ when Hash
106
+ comparisons = comparator
107
+ when String, Regexp
108
+ comparisons[:text] = comparator
109
+ when Integer
110
+ comparisons[:count] = comparator
111
+ when Range
112
+ comparisons[:minimum] = comparator.begin
113
+ comparisons[:maximum] = comparator.end
114
+ when FalseClass
115
+ comparisons[:count] = 0
116
+ when NilClass, TrueClass
117
+ comparisons[:minimum] = 1
118
+ else
119
+ raise ArgumentError, "I don't understand what you're trying to match"
120
+ end
121
+
122
+ if refute
123
+ if comparisons[:count] || (comparisons[:minimum] && !comparator.nil?) || comparisons[:maximum]
124
+ raise ArgumentError, "Cannot use true, false, Integer, Range, :count, :minimum and :maximum when asserting that a selector does not match"
125
+ end
126
+
127
+ comparisons[:count] = 0
128
+ end
129
+
130
+ # By default we're looking for at least one match.
131
+ if comparisons[:count]
132
+ comparisons[:minimum] = comparisons[:maximum] = comparisons[:count]
133
+ else
134
+ comparisons[:minimum] ||= 1
135
+ end
136
+
137
+ if comparisons[:minimum] && comparisons[:maximum] && comparisons[:minimum] > comparisons[:maximum]
138
+ raise ArgumentError, "Range begin or :minimum cannot be greater than Range end or :maximum"
139
+ end
140
+
141
+ @strict = comparisons[:strict]
142
+
143
+ comparisons
144
+ end
145
+
146
+ def collapse_html_whitespace!(text)
147
+ text.gsub!(/\s+/, " ")
148
+ end
149
+ end
150
+ end
151
+ end
111
152
  end
112
- comparisons
113
153
  end
114
154
  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