rspec-html-matchers 0.2.2

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,22 @@
1
+ <table>
2
+ <tr>
3
+ <td>user_1</td>
4
+ <td id="other-special">user_2</td>
5
+ <td>user_3</td>
6
+ </tr>
7
+ <tr>
8
+ <td>a</td>
9
+ <td id="special">a</td>
10
+ <td>a</td>
11
+ </tr>
12
+ </table>
13
+
14
+ <div class="one">text</div>
15
+ <div class="one">text</div>
16
+ <div class="one">text</div>
17
+ <div class="one">text bla</div>
18
+ <div class="one">content bla</div>
19
+ <div class="one">content</div>
20
+ <div class="two">content bla</div>
21
+ <div class="two">content</div>
22
+ <div class="two">text</div>
@@ -0,0 +1,486 @@
1
+ require 'nokogiri'
2
+
3
+ module RSpec
4
+ module Matchers
5
+
6
+ # @api
7
+ # @private
8
+ class NokogiriMatcher
9
+ attr_reader :failure_message, :negative_failure_message
10
+ attr_reader :parent_scope, :current_scope
11
+
12
+ TAG_NOT_FOUND_MSG = %Q|expected following:\n%s\nto have at least 1 element matching "%s", found 0.|
13
+ TAG_FOUND_MSG = %Q|expected following:\n%s\nto NOT have element matching "%s", found %s.|
14
+ WRONG_COUNT_MSG = %Q|expected following:\n%s\nto have %s element(s) matching "%s", found %s.|
15
+ RIGHT_COUNT_MSG = %Q|expected following:\n%s\nto NOT have %s element(s) matching "%s", but found.|
16
+ BETWEEN_COUNT_MSG = %Q|expected following:\n%s\nto have at least %s and at most %s element(s) matching "%s", found %s.|
17
+ RIGHT_BETWEEN_COUNT_MSG = %Q|expected following:\n%s\nto NOT have at least %s and at most %s element(s) matching "%s", but found %s.|
18
+ AT_MOST_MSG = %Q|expected following:\n%s\nto have at most %s element(s) matching "%s", found %s.|
19
+ RIGHT_AT_MOST_MSG = %Q|expected following:\n%s\nto NOT have at most %s element(s) matching "%s", but found %s.|
20
+ AT_LEAST_MSG = %Q|expected following:\n%s\nto have at least %s element(s) matching "%s", found %s.|
21
+ RIGHT_AT_LEAST_MSG = %Q|expected following:\n%s\nto NOT have at least %s element(s) matching "%s", but found %s.|
22
+ REGEXP_NOT_FOUND_MSG = %Q|%s regexp expected within "%s" in following template:\n%s|
23
+ REGEXP_FOUND_MSG = %Q|%s regexp unexpected within "%s" in following template:\n%s\nbut was found.|
24
+ TEXT_NOT_FOUND_MSG = %Q|"%s" expected within "%s" in following template:\n%s|
25
+ TEXT_FOUND_MSG = %Q|"%s" unexpected within "%s" in following template:\n%s\nbut was found.|
26
+ WRONG_COUNT_ERROR_MSG = %Q|:count with :minimum or :maximum has no sence!|
27
+ MIN_MAX_ERROR_MSG = %Q|:minimum shold be less than :maximum!|
28
+ BAD_RANGE_ERROR_MSG = %Q|Your :count range(%s) has no sence!|
29
+
30
+ PRESERVE_WHITESPACE_TAGS = %w( pre textarea )
31
+
32
+ def initialize tag, options={}, &block
33
+ @tag, @options, @block = tag.to_s, options, block
34
+
35
+ if attrs = @options.delete(:with)
36
+ if classes=attrs.delete(:class)
37
+ classes = case classes
38
+ when Array
39
+ classes.join('.')
40
+ when String
41
+ classes.gsub("\s",'.')
42
+ end
43
+ @tag << '.'+classes
44
+ end
45
+ html_attrs_string=''
46
+ attrs.each_pair { |k,v| html_attrs_string << %Q{[#{k.to_s}='#{v.to_s}']} }
47
+ @tag << html_attrs_string
48
+ end
49
+
50
+ validate_options!
51
+ end
52
+
53
+ def matches? document, &block
54
+ @block = block if block
55
+
56
+ case document
57
+ when String
58
+ @parent_scope = @current_scope = Nokogiri::HTML(document).css(@tag)
59
+ @document = document
60
+ else
61
+ @parent_scope = document.current_scope
62
+ @current_scope = document.parent_scope.css(@tag)
63
+ @document = @parent_scope.to_html
64
+ end
65
+
66
+ if tag_presents? and content_right? and count_right?
67
+ @current_scope = @parent_scope
68
+ @block.call if @block
69
+ true
70
+ else
71
+ false
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def tag_presents?
78
+ if @current_scope.first
79
+ @count = @current_scope.count
80
+ @negative_failure_message = TAG_FOUND_MSG % [@document, @tag, @count]
81
+ true
82
+ else
83
+ @failure_message = TAG_NOT_FOUND_MSG % [@document, @tag]
84
+ false
85
+ end
86
+ end
87
+
88
+ def count_right?
89
+ case @options[:count]
90
+ when Integer
91
+ ((@negative_failure_message=RIGHT_COUNT_MSG % [@document,@count,@tag]) && @count == @options[:count]) || (@failure_message=WRONG_COUNT_MSG % [@document,@options[:count],@tag,@count]; false)
92
+ when Range
93
+ ((@negative_failure_message=RIGHT_BETWEEN_COUNT_MSG % [@document,@options[:count].min,@options[:count].max,@tag,@count]) && @options[:count].member?(@count)) || (@failure_message=BETWEEN_COUNT_MSG % [@document,@options[:count].min,@options[:count].max,@tag,@count]; false)
94
+ when nil
95
+ if @options[:maximum]
96
+ ((@negative_failure_message=RIGHT_AT_MOST_MSG % [@document,@options[:maximum],@tag,@count]) && @count <= @options[:maximum]) || (@failure_message=AT_MOST_MSG % [@document,@options[:maximum],@tag,@count]; false)
97
+ elsif @options[:minimum]
98
+ ((@negative_failure_message=RIGHT_AT_LEAST_MSG % [@document,@options[:minimum],@tag,@count]) && @count >= @options[:minimum]) || (@failure_message=AT_LEAST_MSG % [@document,@options[:minimum],@tag,@count]; false)
99
+ else
100
+ true
101
+ end
102
+ end
103
+ end
104
+
105
+ def content_right?
106
+ return true unless @options[:text]
107
+
108
+ case text=@options[:text]
109
+ when Regexp
110
+ new_scope = @current_scope.css(":regexp('#{text}')",Class.new {
111
+ def regexp node_set, text
112
+ node_set.find_all { |node| node.content =~ Regexp.new(text) }
113
+ end
114
+ }.new)
115
+ unless new_scope.empty?
116
+ @count = new_scope.count
117
+ @negative_failure_message = REGEXP_FOUND_MSG % [text.inspect,@tag,@document]
118
+ true
119
+ else
120
+ @failure_message = REGEXP_NOT_FOUND_MSG % [text.inspect,@tag,@document]
121
+ false
122
+ end
123
+ else
124
+ css_param = text.gsub(/'/) { %q{\000027} }
125
+ new_scope = @current_scope.css(":content('#{css_param}')",Class.new {
126
+ def content node_set, text
127
+ match_text = text.gsub(/\\000027/, "'")
128
+ node_set.find_all do |node|
129
+ actual_content = if PRESERVE_WHITESPACE_TAGS.include?(node.name)
130
+ node.content
131
+ else
132
+ node.content.strip.squeeze(' ')
133
+ end
134
+ # remove non-braking spaces:
135
+ actual_content.gsub!("\u00a0", ' ')
136
+ actual_content.gsub!("\302\240", ' ')
137
+ actual_content == match_text
138
+ end
139
+ end
140
+ }.new)
141
+ unless new_scope.empty?
142
+ @count = new_scope.count
143
+ @negative_failure_message = TEXT_FOUND_MSG % [text,@tag,@document]
144
+ true
145
+ else
146
+ @failure_message = TEXT_NOT_FOUND_MSG % [text,@tag,@document]
147
+ false
148
+ end
149
+ end
150
+ end
151
+
152
+ protected
153
+
154
+ def validate_options!
155
+ raise 'wrong :count specified' unless [Range, NilClass].include?(@options[:count].class) or @options[:count].is_a?(Integer)
156
+
157
+ [:min, :minimum, :max, :maximum].each do |key|
158
+ raise WRONG_COUNT_ERROR_MSG if @options.has_key?(key) and @options.has_key?(:count)
159
+ end
160
+
161
+ begin
162
+ raise MIN_MAX_ERROR_MSG if @options[:minimum] > @options[:maximum]
163
+ rescue NoMethodError # nil > 4
164
+ rescue ArgumentError # 2 < nil
165
+ end
166
+
167
+ begin
168
+ begin
169
+ raise BAD_RANGE_ERROR_MSG % [@options[:count].to_s] if @options[:count] && @options[:count].is_a?(Range) && (@options[:count].min.nil? or @options[:count].min < 0)
170
+ rescue ArgumentError, "comparison of String with" # if @options[:count] == 'a'..'z'
171
+ raise BAD_RANGE_ERROR_MSG % [@options[:count].to_s]
172
+ end
173
+ rescue TypeError # fix for 1.8.7 for 'rescue ArgumentError, "comparison of String with"' stroke
174
+ raise BAD_RANGE_ERROR_MSG % [@options[:count].to_s]
175
+ end
176
+
177
+ @options[:minimum] ||= @options.delete(:min)
178
+ @options[:maximum] ||= @options.delete(:max)
179
+
180
+ @options[:text] = @options[:text].to_s if @options.has_key?(:text) && !@options[:text].is_a?(Regexp)
181
+ end
182
+
183
+ end
184
+
185
+ # have_tag matcher
186
+ #
187
+ # @yield block where you should put with_tag
188
+ #
189
+ # @param [String] tag css selector for tag you want to match
190
+ # @param [Hash] options options hash(see below)
191
+ # @option options [Hash] :with hash with other attributes, within this, key :class have special meaning, you may specify it as array of expected classes or string of classes separated by spaces, order does not matter
192
+ # @option options [Fixnum] :count count of matched tags(DO NOT USE :count with :minimum(:min) or :maximum(:max)*)
193
+ # @option options [Range] :count count of matched tags should be between range minimum and maximum values
194
+ # @option options [Fixnum] :minimum minimum count of elements to match
195
+ # @option options [Fixnum] :min same as :minimum
196
+ # @option options [Fixnum] :maximum maximum count of elements to match
197
+ # @option options [Fixnum] :max same as :maximum
198
+ #
199
+ #
200
+ # @example
201
+ # rendered.should have_tag('div')
202
+ # rendered.should have_tag('h1.header')
203
+ # rendered.should have_tag('div#footer')
204
+ # rendered.should have_tag('input#email', :with => { :name => 'user[email]', :type => 'email' } )
205
+ # rendered.should have_tag('div', :count => 3) # matches exactly 3 'div' tags
206
+ # rendered.should have_tag('div', :count => 3..7) # something like have_tag('div', :minimum => 3, :maximum => 7)
207
+ # rendered.should have_tag('div', :minimum => 3) # matches more(or equal) than 3 'div' tags
208
+ # rendered.should have_tag('div', :maximum => 3) # matches less(or equal) than 3 'div' tags
209
+ # rendered.should have_tag('p', :text => 'some content') # will match "<p>some content</p>"
210
+ # rendered.should have_tag('p', :text => /some content/i) # will match "<p>sOme cOntEnt</p>"
211
+ # rendered.should have_tag('textarea', :with => {:name => 'user[description]'}, :text => "I like pie")
212
+ # "<html>
213
+ # <body>
214
+ # <h1>some html document</h1>
215
+ # </body>
216
+ # </html>".should have_tag('body') { with_tag('h1', :text => 'some html document') }
217
+ # '<div class="one two">'.should have_tag('div', :with => { :class => ['two', 'one'] })
218
+ # '<div class="one two">'.should have_tag('div', :with => { :class => 'two one' })
219
+ def have_tag tag, options={}, &block
220
+ if options.kind_of? String
221
+ options = { :text => options }
222
+ end
223
+ @__current_scope_for_nokogiri_matcher = NokogiriMatcher.new(tag, options, &block)
224
+ end
225
+
226
+ # with_tag matcher
227
+ # @yield
228
+ # @see #have_tag
229
+ # @note this should be used within block of have_tag matcher
230
+ def with_tag tag, options={}, &block
231
+ @__current_scope_for_nokogiri_matcher.should have_tag(tag, options, &block)
232
+ end
233
+
234
+ # without_tag matcher
235
+ # @yield
236
+ # @see #have_tag
237
+ # @note this should be used within block of have_tag matcher
238
+ def without_tag tag, options={}, &block
239
+ @__current_scope_for_nokogiri_matcher.should_not have_tag(tag, options, &block)
240
+ end
241
+
242
+ def have_form action_url, method, options={}, &block
243
+ options[:with] ||= {}
244
+ id = options[:with].delete(:id)
245
+ tag = 'form'; tag += '#'+id if id
246
+ options[:with].merge!(:action => action_url)
247
+ options[:with].merge!(:method => method.to_s)
248
+ have_tag tag, options, &block
249
+ end
250
+
251
+ def with_hidden_field name, value=nil
252
+ options = { :with => { :name => name, :type => 'hidden' } }
253
+ options[:with].merge!(:value => value) if value
254
+ should_have_input(options)
255
+ end
256
+
257
+ def without_hidden_field name, value=nil
258
+ options = { :with => { :name => name, :type => 'hidden' } }
259
+ options[:with].merge!(:value => value) if value
260
+ should_not_have_input(options)
261
+ end
262
+
263
+ def with_text_field name, value=nil
264
+ options = { :with => { :name => name, :type => 'text' } }
265
+ options[:with].merge!(:value => value) if value
266
+ should_have_input(options)
267
+ end
268
+
269
+ def without_text_field name, value=nil
270
+ options = { :with => { :name => name, :type => 'text' } }
271
+ options[:with].merge!(:value => value) if value
272
+ should_not_have_input(options)
273
+ end
274
+
275
+ def with_email_field name, value=nil
276
+ options = { :with => { :name => name, :type => 'email' } }
277
+ options[:with].merge!(:value => value) if value
278
+ should_have_input(options)
279
+ end
280
+
281
+ def without_email_field name, value=nil
282
+ options = { :with => { :name => name, :type => 'email' } }
283
+ options[:with].merge!(:value => value) if value
284
+ should_not_have_input(options)
285
+ end
286
+
287
+ def with_url_field name, value=nil
288
+ options = { :with => { :name => name, :type => 'url' } }
289
+ options[:with].merge!(:value => value) if value
290
+ should_have_input(options)
291
+ end
292
+
293
+ def without_url_field name, value=nil
294
+ options = { :with => { :name => name, :type => 'url' } }
295
+ options[:with].merge!(:value => value) if value
296
+ should_not_have_input(options)
297
+ end
298
+
299
+ def with_number_field name, value=nil
300
+ options = { :with => { :name => name, :type => 'number' } }
301
+ options[:with].merge!(:value => value.to_s) if value
302
+ should_have_input(options)
303
+ end
304
+
305
+ def without_number_field name, value=nil
306
+ options = { :with => { :name => name, :type => 'number' } }
307
+ options[:with].merge!(:value => value.to_s) if value
308
+ should_not_have_input(options)
309
+ end
310
+
311
+ def with_range_field name, min, max, options={}
312
+ options = { :with => { :name => name, :type => 'range', :min => min.to_s, :max => max.to_s }.merge(options.delete(:with)||{}) }
313
+ should_have_input(options)
314
+ end
315
+
316
+ def without_range_field name, min=nil, max=nil, options={}
317
+ options = { :with => { :name => name, :type => 'range' }.merge(options.delete(:with)||{}) }
318
+ options[:with].merge!(:min => min.to_s) if min
319
+ options[:with].merge!(:max => max.to_s) if max
320
+ should_not_have_input(options)
321
+ end
322
+
323
+ DATE_FIELD_TYPES = %w( date month week time datetime datetime-local )
324
+
325
+ def with_date_field date_field_type, name=nil, options={}
326
+ date_field_type = date_field_type.to_s
327
+ raise "unknown type `#{date_field_type}` for date picker" unless DATE_FIELD_TYPES.include?(date_field_type)
328
+ options = { :with => { :type => date_field_type.to_s }.merge(options.delete(:with)||{}) }
329
+ options[:with].merge!(:name => name.to_s) if name
330
+ should_have_input(options)
331
+ end
332
+
333
+ def without_date_field date_field_type, name=nil, options={}
334
+ date_field_type = date_field_type.to_s
335
+ raise "unknown type `#{date_field_type}` for date picker" unless DATE_FIELD_TYPES.include?(date_field_type)
336
+ options = { :with => { :type => date_field_type.to_s }.merge(options.delete(:with)||{}) }
337
+ options[:with].merge!(:name => name.to_s) if name
338
+ should_not_have_input(options)
339
+ end
340
+
341
+ def with_password_field name
342
+ options = { :with => { :name => name, :type => 'password' } }
343
+ should_have_input(options)
344
+ end
345
+
346
+ def without_password_field name
347
+ options = { :with => { :name => name, :type => 'password' } }
348
+ should_not_have_input(options)
349
+ end
350
+
351
+ def with_file_field name
352
+ options = { :with => { :name => name, :type => 'file' } }
353
+ should_have_input(options)
354
+ end
355
+
356
+ def without_file_field name
357
+ options = { :with => { :name => name, :type => 'file' } }
358
+ should_not_have_input(options)
359
+ end
360
+
361
+ def with_text_area name
362
+ options = { :with => { :name => name } }
363
+ @__current_scope_for_nokogiri_matcher.should have_tag('textarea', options)
364
+ end
365
+
366
+ def without_text_area name
367
+ options = { :with => { :name => name } }
368
+ @__current_scope_for_nokogiri_matcher.should_not have_tag('textarea', options)
369
+ end
370
+
371
+ def with_checkbox name, value=nil
372
+ options = { :with => { :name => name, :type => 'checkbox' } }
373
+ options[:with].merge!(:value => value) if value
374
+ should_have_input(options)
375
+ end
376
+
377
+ def without_checkbox name, value=nil
378
+ options = { :with => { :name => name, :type => 'checkbox' } }
379
+ options[:with].merge!(:value => value) if value
380
+ should_not_have_input(options)
381
+ end
382
+
383
+ def with_radio_button name, value
384
+ options = { :with => { :name => name, :type => 'radio' } }
385
+ options[:with].merge!(:value => value)
386
+ should_have_input(options)
387
+ end
388
+
389
+ def without_radio_button name, value
390
+ options = { :with => { :name => name, :type => 'radio' } }
391
+ options[:with].merge!(:value => value)
392
+ should_not_have_input(options)
393
+ end
394
+
395
+ def with_select name, options={}, &block
396
+ options[:with] ||= {}
397
+ id = options[:with].delete(:id)
398
+ tag='select'; tag += '#'+id if id
399
+ options[:with].merge!(:name => name)
400
+ @__current_scope_for_nokogiri_matcher.should have_tag(tag, options, &block)
401
+ end
402
+
403
+ def without_select name, options={}, &block
404
+ options[:with] ||= {}
405
+ id = options[:with].delete(:id)
406
+ tag='select'; tag += '#'+id if id
407
+ options[:with].merge!(:name => name)
408
+ @__current_scope_for_nokogiri_matcher.should_not have_tag(tag, options, &block)
409
+ end
410
+
411
+ def with_option text, value=nil, options={}
412
+ options[:with] ||= {}
413
+ if value.is_a?(Hash)
414
+ options.merge!(value)
415
+ value=nil
416
+ end
417
+ tag='option'
418
+ options[:with].merge!(:value => value.to_s) if value
419
+ if options[:selected]
420
+ options[:with].merge!(:selected => "selected")
421
+ end
422
+ options.delete(:selected)
423
+ options.merge!(:text => text) if text
424
+ @__current_scope_for_nokogiri_matcher.should have_tag(tag, options)
425
+ end
426
+
427
+ def without_option text, value=nil, options={}
428
+ options[:with] ||= {}
429
+ if value.is_a?(Hash)
430
+ options.merge!(value)
431
+ value=nil
432
+ end
433
+ tag='option'
434
+ options[:with].merge!(:value => value.to_s) if value
435
+ if options[:selected]
436
+ options[:with].merge!(:selected => "selected")
437
+ end
438
+ options.delete(:selected)
439
+ options.merge!(:text => text) if text
440
+ @__current_scope_for_nokogiri_matcher.should_not have_tag(tag, options)
441
+ end
442
+
443
+ def with_button text, value=nil, options={}
444
+ options[:with] ||= {}
445
+ if value.is_a?(Hash)
446
+ options.merge!(value)
447
+ value=nil
448
+ end
449
+ options[:with].merge!(:value => value.to_s) if value
450
+ options.merge!(:text => text) if text
451
+ @__current_scope_for_nokogiri_matcher.should have_tag('button', options)
452
+ end
453
+
454
+ def without_button text, value=nil, options={}
455
+ options[:with] ||= {}
456
+ if value.is_a?(Hash)
457
+ options.merge!(value)
458
+ value=nil
459
+ end
460
+ options[:with].merge!(:value => value.to_s) if value
461
+ options.merge!(:text => text) if text
462
+ @__current_scope_for_nokogiri_matcher.should_not have_tag('button', options)
463
+ end
464
+
465
+ def with_submit value
466
+ options = { :with => { :type => 'submit', :value => value } }
467
+ should_have_input(options)
468
+ end
469
+
470
+ def without_submit value
471
+ options = { :with => { :type => 'submit', :value => value } }
472
+ should_not_have_input(options)
473
+ end
474
+
475
+ private
476
+
477
+ def should_have_input(options)
478
+ @__current_scope_for_nokogiri_matcher.should have_tag('input', options)
479
+ end
480
+
481
+ def should_not_have_input(options)
482
+ @__current_scope_for_nokogiri_matcher.should_not have_tag('input', options)
483
+ end
484
+
485
+ end
486
+ end