rspec-html-matchers 0.7.0 → 0.10.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.
@@ -0,0 +1,290 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'nokogiri'
5
+
6
+ module RSpecHtmlMatchers
7
+ # @api
8
+ # @private
9
+ class HaveTag # rubocop:disable Metrics/ClassLength
10
+ DESCRIPTIONS = {
11
+ :have_at_least_1 => %(have at least 1 element matching "%s"),
12
+ :have_n => %(have %i element(s) matching "%s"),
13
+ }.freeze
14
+
15
+ MESSAGES = {
16
+ :expected_tag => %(expected following:\n%s\nto #{DESCRIPTIONS[:have_at_least_1]}, found 0.),
17
+ :unexpected_tag => %(expected following:\n%s\nto NOT have element matching "%s", found %s.),
18
+
19
+ :expected_count => %(expected following:\n%s\nto #{DESCRIPTIONS[:have_n]}, found %s.),
20
+ :unexpected_count => %(expected following:\n%s\nto NOT have %i element(s) matching "%s", but found.),
21
+
22
+ :expected_btw_count => %(expected following:\n%s\nto have at least %i and at most %i element(s) matching "%s", found %i.),
23
+ :unexpected_btw_count => %(expected following:\n%s\nto NOT have at least %i and at most %i element(s) matching "%s", but found %i.),
24
+
25
+ :expected_at_most => %(expected following:\n%s\nto have at most %i element(s) matching "%s", found %i.),
26
+ :unexpected_at_most => %(expected following:\n%s\nto NOT have at most %i element(s) matching "%s", but found %i.),
27
+
28
+ :expected_at_least => %(expected following:\n%s\nto have at least %i element(s) matching "%s", found %i.),
29
+ :unexpected_at_least => %(expected following:\n%s\nto NOT have at least %i element(s) matching "%s", but found %i.),
30
+
31
+ :expected_blank => %(expected following template to contain empty tag %s:\n%s),
32
+ :unexpected_blank => %(expected following template to contain tag %s with other tags:\n%s),
33
+
34
+ :expected_regexp => %(%s regexp expected within "%s" in following template:\n%s),
35
+ :unexpected_regexp => %(%s regexp unexpected within "%s" in following template:\n%s\nbut was found.),
36
+
37
+ :expected_text => %("%s" expected within "%s" in following template:\n%s),
38
+ :unexpected_text => %("%s" unexpected within "%s" in following template:\n%s\nbut was found.),
39
+
40
+ :wrong_count_error => %(:count with :minimum or :maximum has no sence!),
41
+ :min_max_error => %(:minimum should be less than :maximum!),
42
+ :bad_range_error => %(Your :count range(%s) has no sence!),
43
+ }.freeze
44
+
45
+ def initialize tag, options = {}, &block
46
+ @tag = tag.to_s
47
+ @options = options
48
+ @block = block
49
+
50
+ if with_attrs = @options.delete(:with)
51
+ if classes = with_attrs.delete(:class)
52
+ @tag += '.' + classes_to_selector(classes)
53
+ end
54
+ selector = with_attrs.inject('') do |html_attrs_string, (k, v)|
55
+ html_attrs_string += "[#{k}='#{v}']"
56
+ html_attrs_string
57
+ end
58
+ @tag += selector
59
+ end
60
+
61
+ if without_attrs = @options.delete(:without)
62
+ if classes = without_attrs.delete(:class)
63
+ @tag += ":not(.#{classes_to_selector(classes)})"
64
+ end
65
+ end
66
+
67
+ validate_options!
68
+ organize_options!
69
+ end
70
+
71
+ attr_reader :failure_message
72
+ attr_reader :failure_message_when_negated
73
+ attr_reader :current_scope
74
+
75
+ def matches? src, &block
76
+ @block = block if block
77
+
78
+ src = src.html if defined?(Capybara::Session) && src.is_a?(Capybara::Session)
79
+
80
+ case src
81
+ when String
82
+ parent_scope = Nokogiri::HTML(src)
83
+ @document = src
84
+ else
85
+ parent_scope = src.current_scope
86
+ @document = parent_scope.to_html
87
+ end
88
+
89
+ @current_scope = begin
90
+ parent_scope.css(tag)
91
+ # on jruby this produce exception if css was not found:
92
+ # undefined method `decorate' for nil:NilClass
93
+ rescue NoMethodError
94
+ Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new)
95
+ end
96
+ if tag_presents? && proper_content? && count_right?
97
+ @block.call(self) if @block
98
+ true
99
+ else
100
+ false
101
+ end
102
+ end
103
+
104
+ def description
105
+ # TODO: should it be more complicated?
106
+ if options.key?(:count)
107
+ format(DESCRIPTIONS[:have_n], options[:count], tag)
108
+ else
109
+ DESCRIPTIONS[:have_at_least_1] % tag
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ attr_reader :tag
116
+ attr_reader :options
117
+ attr_reader :document
118
+
119
+ def classes_to_selector classes
120
+ case classes
121
+ when Array
122
+ classes.join('.')
123
+ when String
124
+ classes.gsub(/\s+/, '.')
125
+ end
126
+ end
127
+
128
+ def tag_presents?
129
+ if current_scope.first
130
+ @count = current_scope.count
131
+ match_succeeded! :unexpected_tag, document, tag, @count
132
+ else
133
+ match_failed! :expected_tag, document, tag
134
+ end
135
+ end
136
+
137
+ def count_right? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
138
+ case options[:count]
139
+ when Integer
140
+ if @count == options[:count]
141
+ match_succeeded! :unexpected_count, document, @count, tag
142
+ else
143
+ match_failed! :expected_count, document, options[:count], tag, @count
144
+ end
145
+ when Range
146
+ if options[:count].member? @count
147
+ match_succeeded! :unexpected_btw_count, document, options[:count].min, options[:count].max, tag, @count
148
+ else
149
+ match_failed! :expected_btw_count, document, options[:count].min, options[:count].max, tag, @count
150
+ end
151
+ when nil
152
+ if options[:maximum]
153
+ if @count <= options[:maximum]
154
+ match_succeeded! :unexpected_at_most, document, options[:maximum], tag, @count
155
+ else
156
+ match_failed! :expected_at_most, document, options[:maximum], tag, @count
157
+ end
158
+ elsif options[:minimum]
159
+ if @count >= options[:minimum]
160
+ match_succeeded! :unexpected_at_least, document, options[:minimum], tag, @count
161
+ else
162
+ match_failed! :expected_at_least, document, options[:minimum], tag, @count
163
+ end
164
+ else
165
+ true
166
+ end
167
+ end
168
+ end
169
+
170
+ def proper_content?
171
+ if options.key?(:blank)
172
+ maybe_empty?
173
+ else
174
+ text_right?
175
+ end
176
+ end
177
+
178
+ def maybe_empty?
179
+ if options[:blank] && current_scope.children.empty?
180
+ match_succeeded! :unexpected_blank, tag, document
181
+ else
182
+ match_failed! :expected_blank, tag, document
183
+ end
184
+ end
185
+
186
+ def text_right?
187
+ return true unless options[:text]
188
+
189
+ case text = options[:text]
190
+ when Regexp
191
+ new_scope = current_scope.css(':regexp()', NokogiriRegexpHelper.new(text))
192
+ if new_scope.empty?
193
+ match_failed! :expected_regexp, text.inspect, tag, document
194
+ else
195
+ @count = new_scope.count
196
+ match_succeeded! :unexpected_regexp, text.inspect, tag, document
197
+ end
198
+ else
199
+ new_scope = current_scope.css(':content()', NokogiriTextHelper.new(text, options[:squeeze_text]))
200
+ if new_scope.empty?
201
+ match_failed! :expected_text, text, tag, document
202
+ else
203
+ @count = new_scope.count
204
+ match_succeeded! :unexpected_text, text, tag, document
205
+ end
206
+ end
207
+ end
208
+
209
+ def validate_options!
210
+ validate_html_body_tags!
211
+ validate_text_options!
212
+ validate_count_presence!
213
+ validate_count_when_set_min_max!
214
+ validate_count_when_set_range!
215
+ end
216
+
217
+ # here is a demo:
218
+ # irb(main):009:0> Nokogiri::HTML('<p>asd</p>').xpath('//html')
219
+ # => [#<Nokogiri::XML::Element:0x3fea02cd3f58 name="html" children=[#<Nokogiri::XML::Element:0x3fea02cd37c4 name="body" children=[#<Nokogiri::XML::Element:0x3fea02cd34e0 name="p" children=[#<Nokogiri::XML::Text:0x3fea02cd3134 "asd">]>]>]>]
220
+ # irb(main):010:0> Nokogiri::HTML('<p>asd</p>').xpath('//body')
221
+ # => [#<Nokogiri::XML::Element:0x3fea02ce3df4 name="body" children=[#<Nokogiri::XML::Element:0x3fea02ce3a70 name="p" children=[#<Nokogiri::XML::Text:0x3fea02ce350c "asd">]>]>]
222
+ # irb(main):011:0> Nokogiri::HTML('<p>asd</p>').xpath('//p')
223
+ # => [#<Nokogiri::XML::Element:0x3fea02cf3754 name="p" children=[#<Nokogiri::XML::Text:0x3fea02cf2f98 "asd">]>]
224
+ # irb(main):012:0> Nokogiri::HTML('<p>asd</p>').xpath('//a')
225
+ # => []
226
+ def validate_html_body_tags!
227
+ if %w[html body].include?(tag) && options.empty?
228
+ raise ArgumentError, 'matching <html> and <body> tags without specifying additional options does not work, see: https://github.com/kucaahbe/rspec-html-matchers/pull/75'
229
+ end
230
+ end
231
+
232
+ def validate_text_options!
233
+ # TODO: test these options validations
234
+ if options.key?(:blank) && options[:blank] && options.key?(:text) # rubocop:disable Style/GuardClause, Style/IfUnlessModifier
235
+ raise ':text option is not accepted when :blank => true'
236
+ end
237
+ end
238
+
239
+ def validate_count_presence!
240
+ raise 'wrong :count specified' unless [Range, NilClass].include?(options[:count].class) || options[:count].is_a?(Integer)
241
+
242
+ [:min, :minimum, :max, :maximum].each do |key|
243
+ raise MESSAGES[:wrong_count_error] if options.key?(key) && options.key?(:count)
244
+ end
245
+ end
246
+
247
+ def validate_count_when_set_min_max!
248
+ raise MESSAGES[:min_max_error] if options[:minimum] > options[:maximum]
249
+ rescue NoMethodError # nil > 4 # rubocop:disable Lint/HandleExceptions
250
+ rescue ArgumentError # 2 < nil # rubocop:disable Lint/HandleExceptions
251
+ end
252
+
253
+ def validate_count_when_set_range!
254
+ begin
255
+ raise format(MESSAGES[:bad_range_error], options[:count].to_s) if count_is_range_but_no_min?
256
+ rescue ArgumentError, 'comparison of String with' # if options[:count] == 'a'..'z' # rubocop:disable Lint/RescueType
257
+ raise format(MESSAGES[:bad_range_error], options[:count].to_s)
258
+ end
259
+ rescue TypeError # fix for 1.8.7 for 'rescue ArgumentError, "comparison of String with"' stroke
260
+ raise format(MESSAGES[:bad_range_error], options[:count].to_s)
261
+ end
262
+
263
+ def count_is_range_but_no_min?
264
+ options[:count].is_a?(Range) &&
265
+ (options[:count].min.nil? || (options[:count].min < 0))
266
+ end
267
+
268
+ def organize_options!
269
+ @options[:minimum] ||= @options.delete(:min)
270
+ @options[:maximum] ||= @options.delete(:max)
271
+
272
+ @options[:text] = @options[:text].to_s if @options.key?(:text) && !@options[:text].is_a?(Regexp)
273
+
274
+ if @options.key?(:seen) && !@options[:seen].is_a?(Regexp) # rubocop:disable Style/GuardClause
275
+ @options[:text] = @options[:seen].to_s
276
+ @options[:squeeze_text] = true
277
+ end
278
+ end
279
+
280
+ def match_succeeded! message, *args
281
+ @failure_message_when_negated = format MESSAGES[message], *args
282
+ true
283
+ end
284
+
285
+ def match_failed! message, *args
286
+ @failure_message = format MESSAGES[message], *args
287
+ false
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module RSpecHtmlMatchers
5
+ # @api
6
+ # @private
7
+ # for nokogiri regexp matching
8
+ class NokogiriRegexpHelper
9
+ def initialize regex
10
+ @regex = regex
11
+ end
12
+
13
+ def regexp node_set
14
+ node_set.find_all { |node| node.content =~ @regex }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module RSpecHtmlMatchers
5
+ # @api
6
+ # @private
7
+ class NokogiriTextHelper
8
+ NON_BREAKING_SPACE = "\u00a0"
9
+
10
+ def initialize text, squeeze_text = false
11
+ @text = text
12
+ @squeeze_text = squeeze_text
13
+ end
14
+
15
+ def content node_set
16
+ node_set.find_all do |node|
17
+ actual_content = node.content.gsub(NON_BREAKING_SPACE, ' ')
18
+ actual_content = node.content.gsub(/\s+/, ' ').strip if @squeeze_text
19
+
20
+ actual_content == @text
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHtmlMatchers
4
+ VERSION = '0.10.0'
5
+ end