rspec-html-matchers 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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