rspec-html-matchers 0.6.1 → 0.7.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +28 -0
- data/features/support/env.rb +2 -0
- data/lib/rspec-html-matchers.rb +448 -458
- data/spec/spec_helper.rb +2 -0
- metadata +36 -68
- data/spec/fixtures/form.html +0 -140
- data/spec/fixtures/ordered_list.html +0 -9
- data/spec/fixtures/paragraphs.html +0 -3
- data/spec/fixtures/quotes.html +0 -15
- data/spec/fixtures/search_and_submit.html +0 -9
- data/spec/fixtures/single_element.html +0 -1
- data/spec/fixtures/special.html +0 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed044ed780b0ef4fefc403e3d644e6759b45232e
|
4
|
+
data.tar.gz: 3a9a74c22ee2c89bfae15fd3de267b3cf048604e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 52855bb634b6ccf3a31995e7d3a7730410d3f7c3209b9bfe9a42b9c924db3b7eb7d028d95a5523d1c02e87eab4929aad565a5483e7e1f1101a8eee8bad55cc2c
|
7
|
+
data.tar.gz: ecedc6b4e6791348db17d86a0338ccdeccfa3aa35e97a99a641358247cae14c89a39c6f2a56f026eba9b0f1d94b8f3757cc87b3186d8aa59a96ddfcb58fa3cac
|
data/CHANGELOG.md
CHANGED
@@ -11,6 +11,18 @@ unreleased(TODO)
|
|
11
11
|
* order matching
|
12
12
|
* improve documentation, add more usage examples (look at changelog and code!)
|
13
13
|
|
14
|
+
0.7.0
|
15
|
+
-----
|
16
|
+
|
17
|
+
* new, explicit configuration, refer to README
|
18
|
+
* added ruby 2.2.0 to CI
|
19
|
+
|
20
|
+
0.6.1
|
21
|
+
-----
|
22
|
+
|
23
|
+
* rspec 3 version update
|
24
|
+
* added ruby 2.1.2 to CI
|
25
|
+
|
14
26
|
0.6.0
|
15
27
|
-----
|
16
28
|
|
data/README.md
CHANGED
@@ -28,6 +28,33 @@ Add to your Gemfile in the `:test` group:
|
|
28
28
|
gem 'rspec-html-matchers'
|
29
29
|
```
|
30
30
|
|
31
|
+
and somewhere in RSpec configuration:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
RSpec.configure do |config|
|
35
|
+
config.include RSpecHtmlMatchers
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
or just in you spec(s):
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
describe "my view spec" do
|
43
|
+
include RSpecHtmlMatchers
|
44
|
+
|
45
|
+
it "has tags" do
|
46
|
+
expect(rendered).to have_tag('div')
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Cucumber configuration:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
World RSpecHtmlMatchers
|
56
|
+
```
|
57
|
+
|
31
58
|
as this gem requires **nokogiri**, here [instructions for installing it](http://nokogiri.org/tutorials/installing_nokogiri.html).
|
32
59
|
|
33
60
|
Usage
|
@@ -195,6 +222,7 @@ Contributors
|
|
195
222
|
- [Felix Tjandrawibawa](https://github.com/cemenghttps://github.com/cemeng)
|
196
223
|
- [Szymon Przybył](https://github.com/apocalyptiq)
|
197
224
|
- [Manuel Meurer](https://github.com/manuelmeurer)
|
225
|
+
- [Andreas Riemer](https://github.com/arfl)
|
198
226
|
|
199
227
|
MIT Licensed
|
200
228
|
============
|
data/features/support/env.rb
CHANGED
data/lib/rspec-html-matchers.rb
CHANGED
@@ -2,568 +2,558 @@
|
|
2
2
|
require 'rspec'
|
3
3
|
require 'nokogiri'
|
4
4
|
|
5
|
-
module
|
6
|
-
module HtmlMatchers
|
7
|
-
|
8
|
-
# @api
|
9
|
-
# @private
|
10
|
-
# for nokogiri regexp matching
|
11
|
-
class NokogiriRegexpHelper
|
12
|
-
def initialize(regex)
|
13
|
-
@regex = regex
|
14
|
-
end
|
5
|
+
module RSpecHtmlMatchers
|
15
6
|
|
16
|
-
|
17
|
-
|
18
|
-
|
7
|
+
# @api
|
8
|
+
# @private
|
9
|
+
# for nokogiri regexp matching
|
10
|
+
class NokogiriRegexpHelper
|
11
|
+
def initialize(regex)
|
12
|
+
@regex = regex
|
19
13
|
end
|
20
14
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
15
|
+
def regexp node_set
|
16
|
+
node_set.find_all { |node| node.content =~ @regex }
|
17
|
+
end
|
18
|
+
end
|
25
19
|
|
26
|
-
|
27
|
-
|
28
|
-
|
20
|
+
# @api
|
21
|
+
# @private
|
22
|
+
class NokogiriTextHelper
|
23
|
+
NON_BREAKING_SPACE = "\u00a0"
|
29
24
|
|
30
|
-
|
31
|
-
|
32
|
-
|
25
|
+
def initialize text
|
26
|
+
@text = text
|
27
|
+
end
|
33
28
|
|
34
|
-
|
35
|
-
|
29
|
+
def content node_set
|
30
|
+
node_set.find_all do |node|
|
31
|
+
actual_content = node.content.gsub(NON_BREAKING_SPACE, ' ')
|
32
|
+
|
33
|
+
actual_content == @text
|
36
34
|
end
|
37
35
|
end
|
36
|
+
end
|
38
37
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
# @api
|
39
|
+
# @private
|
40
|
+
class HaveTag
|
41
|
+
attr_reader :failure_message, :failure_message_when_negated
|
42
|
+
attr_reader :parent_scope, :current_scope
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
44
|
+
DESCRIPTIONS = {
|
45
|
+
:have_at_least_1 => %Q|have at least 1 element matching "%s"|,
|
46
|
+
:have_n => %Q|have %i element(s) matching "%s"|
|
47
|
+
}
|
49
48
|
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
MESSAGES = {
|
50
|
+
:expected_tag => %Q|expected following:\n%s\nto #{DESCRIPTIONS[:have_at_least_1]}, found 0.|,
|
51
|
+
:unexpected_tag => %Q|expected following:\n%s\nto NOT have element matching "%s", found %s.|,
|
53
52
|
|
54
|
-
|
55
|
-
|
53
|
+
:expected_count => %Q|expected following:\n%s\nto #{DESCRIPTIONS[:have_n]}, found %s.|,
|
54
|
+
:unexpected_count => %Q|expected following:\n%s\nto NOT have %i element(s) matching "%s", but found.|,
|
56
55
|
|
57
|
-
|
58
|
-
|
56
|
+
:expected_btw_count => %Q|expected following:\n%s\nto have at least %i and at most %i element(s) matching "%s", found %i.|,
|
57
|
+
:unexpected_btw_count => %Q|expected following:\n%s\nto NOT have at least %i and at most %i element(s) matching "%s", but found %i.|,
|
59
58
|
|
60
|
-
|
61
|
-
|
59
|
+
:expected_at_most => %Q|expected following:\n%s\nto have at most %i element(s) matching "%s", found %i.|,
|
60
|
+
:unexpected_at_most => %Q|expected following:\n%s\nto NOT have at most %i element(s) matching "%s", but found %i.|,
|
62
61
|
|
63
|
-
|
64
|
-
|
62
|
+
:expected_at_least => %Q|expected following:\n%s\nto have at least %i element(s) matching "%s", found %i.|,
|
63
|
+
:unexpected_at_least => %Q|expected following:\n%s\nto NOT have at least %i element(s) matching "%s", but found %i.|,
|
65
64
|
|
66
|
-
|
67
|
-
|
65
|
+
:expected_regexp => %Q|%s regexp expected within "%s" in following template:\n%s|,
|
66
|
+
:unexpected_regexp => %Q|%s regexp unexpected within "%s" in following template:\n%s\nbut was found.|,
|
68
67
|
|
69
|
-
|
70
|
-
|
68
|
+
:expected_text => %Q|"%s" expected within "%s" in following template:\n%s|,
|
69
|
+
:unexpected_text => %Q|"%s" unexpected within "%s" in following template:\n%s\nbut was found.|,
|
71
70
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
71
|
+
:wrong_count_error => %Q|:count with :minimum or :maximum has no sence!|,
|
72
|
+
:min_max_error => %Q|:minimum should be less than :maximum!|,
|
73
|
+
:bad_range_error => %Q|Your :count range(%s) has no sence!|,
|
74
|
+
}
|
76
75
|
|
77
76
|
|
78
|
-
|
79
|
-
|
77
|
+
def initialize tag, options={}, &block
|
78
|
+
@tag, @options, @block = tag.to_s, options, block
|
80
79
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
end
|
85
|
-
selector = with_attrs.inject('') do |html_attrs_string, (k, v)|
|
86
|
-
html_attrs_string << "[#{k}='#{v}']"
|
87
|
-
html_attrs_string
|
88
|
-
end
|
89
|
-
@tag << selector
|
80
|
+
if with_attrs = @options.delete(:with)
|
81
|
+
if classes = with_attrs.delete(:class)
|
82
|
+
@tag << '.' + classes_to_selector(classes)
|
90
83
|
end
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
@tag << ":not(.#{classes_to_selector(classes)})"
|
95
|
-
end
|
84
|
+
selector = with_attrs.inject('') do |html_attrs_string, (k, v)|
|
85
|
+
html_attrs_string << "[#{k}='#{v}']"
|
86
|
+
html_attrs_string
|
96
87
|
end
|
88
|
+
@tag << selector
|
89
|
+
end
|
97
90
|
|
98
|
-
|
91
|
+
if without_attrs = @options.delete(:without)
|
92
|
+
if classes = without_attrs.delete(:class)
|
93
|
+
@tag << ":not(.#{classes_to_selector(classes)})"
|
94
|
+
end
|
99
95
|
end
|
100
96
|
|
101
|
-
|
102
|
-
|
97
|
+
validate_options!
|
98
|
+
end
|
103
99
|
|
104
|
-
|
100
|
+
def matches? document, &block
|
101
|
+
@block = block if block
|
105
102
|
|
106
|
-
|
107
|
-
when String
|
108
|
-
@parent_scope = @current_scope = Nokogiri::HTML(document).css(@tag)
|
109
|
-
@document = document
|
110
|
-
else
|
111
|
-
@parent_scope = document.current_scope
|
112
|
-
@current_scope = begin
|
113
|
-
document.parent_scope.css(@tag)
|
114
|
-
# on jruby this produce exception if css was not found:
|
115
|
-
# undefined method `decorate' for nil:NilClass
|
116
|
-
rescue NoMethodError
|
117
|
-
Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new)
|
118
|
-
end
|
119
|
-
@document = @parent_scope.to_html
|
120
|
-
end
|
103
|
+
document = document.html if defined?(Capybara::Session) && document.is_a?(Capybara::Session)
|
121
104
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
105
|
+
case document
|
106
|
+
when String
|
107
|
+
@parent_scope = @current_scope = Nokogiri::HTML(document).css(@tag)
|
108
|
+
@document = document
|
109
|
+
else
|
110
|
+
@parent_scope = document.current_scope
|
111
|
+
@current_scope = begin
|
112
|
+
document.parent_scope.css(@tag)
|
113
|
+
# on jruby this produce exception if css was not found:
|
114
|
+
# undefined method `decorate' for nil:NilClass
|
115
|
+
rescue NoMethodError
|
116
|
+
Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new)
|
117
|
+
end
|
118
|
+
@document = @parent_scope.to_html
|
129
119
|
end
|
130
120
|
|
131
|
-
|
132
|
-
|
133
|
-
if @
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
121
|
+
if tag_presents? and text_right? and count_right?
|
122
|
+
@current_scope = @parent_scope
|
123
|
+
@block.call if @block
|
124
|
+
true
|
125
|
+
else
|
126
|
+
false
|
138
127
|
end
|
128
|
+
end
|
139
129
|
|
140
|
-
|
130
|
+
def description
|
131
|
+
# TODO should it be more complicated?
|
132
|
+
if @options.has_key?(:count)
|
133
|
+
DESCRIPTIONS[:have_n] % [@options[:count],@tag]
|
134
|
+
else
|
135
|
+
DESCRIPTIONS[:have_at_least_1] % @tag
|
136
|
+
end
|
137
|
+
end
|
141
138
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
139
|
+
private
|
140
|
+
|
141
|
+
def classes_to_selector(classes)
|
142
|
+
case classes
|
143
|
+
when Array
|
144
|
+
classes.join('.')
|
145
|
+
when String
|
146
|
+
classes.gsub(/\s+/, '.')
|
149
147
|
end
|
148
|
+
end
|
150
149
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end
|
150
|
+
def tag_presents?
|
151
|
+
if @current_scope.first
|
152
|
+
@count = @current_scope.count
|
153
|
+
@failure_message_when_negated = MESSAGES[:unexpected_tag] % [@document, @tag, @count]
|
154
|
+
true
|
155
|
+
else
|
156
|
+
@failure_message = MESSAGES[:expected_tag] % [@document, @tag]
|
157
|
+
false
|
160
158
|
end
|
159
|
+
end
|
161
160
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
161
|
+
def count_right?
|
162
|
+
case @options[:count]
|
163
|
+
when Integer
|
164
|
+
((@failure_message_when_negated=MESSAGES[:unexpected_count] % [@document,@count,@tag]) && @count == @options[:count]) || (@failure_message=MESSAGES[:expected_count] % [@document,@options[:count],@tag,@count]; false)
|
165
|
+
when Range
|
166
|
+
((@failure_message_when_negated=MESSAGES[:unexpected_btw_count] % [@document,@options[:count].min,@options[:count].max,@tag,@count]) && @options[:count].member?(@count)) || (@failure_message=MESSAGES[:expected_btw_count] % [@document,@options[:count].min,@options[:count].max,@tag,@count]; false)
|
167
|
+
when nil
|
168
|
+
if @options[:maximum]
|
169
|
+
((@failure_message_when_negated=MESSAGES[:unexpected_at_most] % [@document,@options[:maximum],@tag,@count]) && @count <= @options[:maximum]) || (@failure_message=MESSAGES[:expected_at_most] % [@document,@options[:maximum],@tag,@count]; false)
|
170
|
+
elsif @options[:minimum]
|
171
|
+
((@failure_message_when_negated=MESSAGES[:unexpected_at_least] % [@document,@options[:minimum],@tag,@count]) && @count >= @options[:minimum]) || (@failure_message=MESSAGES[:expected_at_least] % [@document,@options[:minimum],@tag,@count]; false)
|
172
|
+
else
|
173
|
+
true
|
176
174
|
end
|
177
175
|
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def text_right?
|
179
|
+
return true unless @options[:text]
|
178
180
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
@count = new_scope.count
|
187
|
-
@failure_message_when_negated = MESSAGES[:unexpected_regexp] % [text.inspect,@tag,@document]
|
188
|
-
true
|
189
|
-
else
|
190
|
-
@failure_message = MESSAGES[:expected_regexp] % [text.inspect,@tag,@document]
|
191
|
-
false
|
192
|
-
end
|
181
|
+
case text=@options[:text]
|
182
|
+
when Regexp
|
183
|
+
new_scope = @current_scope.css(':regexp()',NokogiriRegexpHelper.new(text))
|
184
|
+
unless new_scope.empty?
|
185
|
+
@count = new_scope.count
|
186
|
+
@failure_message_when_negated = MESSAGES[:unexpected_regexp] % [text.inspect,@tag,@document]
|
187
|
+
true
|
193
188
|
else
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
189
|
+
@failure_message = MESSAGES[:expected_regexp] % [text.inspect,@tag,@document]
|
190
|
+
false
|
191
|
+
end
|
192
|
+
else
|
193
|
+
new_scope = @current_scope.css(':content()',NokogiriTextHelper.new(text))
|
194
|
+
unless new_scope.empty?
|
195
|
+
@count = new_scope.count
|
196
|
+
@failure_message_when_negated = MESSAGES[:unexpected_text] % [text,@tag,@document]
|
197
|
+
true
|
198
|
+
else
|
199
|
+
@failure_message = MESSAGES[:expected_text] % [text,@tag,@document]
|
200
|
+
false
|
203
201
|
end
|
204
202
|
end
|
203
|
+
end
|
205
204
|
|
206
|
-
|
205
|
+
protected
|
207
206
|
|
208
|
-
|
209
|
-
|
207
|
+
def validate_options!
|
208
|
+
raise 'wrong :count specified' unless [Range, NilClass].include?(@options[:count].class) or @options[:count].is_a?(Integer)
|
210
209
|
|
211
|
-
|
212
|
-
|
213
|
-
|
210
|
+
[:min, :minimum, :max, :maximum].each do |key|
|
211
|
+
raise MESSAGES[:wrong_count_error] if @options.has_key?(key) and @options.has_key?(:count)
|
212
|
+
end
|
214
213
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
214
|
+
begin
|
215
|
+
raise MESSAGES[:min_max_error] if @options[:minimum] > @options[:maximum]
|
216
|
+
rescue NoMethodError # nil > 4
|
217
|
+
rescue ArgumentError # 2 < nil
|
218
|
+
end
|
220
219
|
|
220
|
+
begin
|
221
221
|
begin
|
222
|
-
|
223
|
-
|
224
|
-
rescue ArgumentError, "comparison of String with" # if @options[:count] == 'a'..'z'
|
225
|
-
raise MESSAGES[:bad_range_error] % [@options[:count].to_s]
|
226
|
-
end
|
227
|
-
rescue TypeError # fix for 1.8.7 for 'rescue ArgumentError, "comparison of String with"' stroke
|
222
|
+
raise MESSAGES[:bad_range_error] % [@options[:count].to_s] if @options[:count] && @options[:count].is_a?(Range) && (@options[:count].min.nil? or @options[:count].min < 0)
|
223
|
+
rescue ArgumentError, "comparison of String with" # if @options[:count] == 'a'..'z'
|
228
224
|
raise MESSAGES[:bad_range_error] % [@options[:count].to_s]
|
229
225
|
end
|
230
|
-
|
231
|
-
|
232
|
-
@options[:maximum] ||= @options.delete(:max)
|
233
|
-
|
234
|
-
@options[:text] = @options[:text].to_s if @options.has_key?(:text) && !@options[:text].is_a?(Regexp)
|
226
|
+
rescue TypeError # fix for 1.8.7 for 'rescue ArgumentError, "comparison of String with"' stroke
|
227
|
+
raise MESSAGES[:bad_range_error] % [@options[:count].to_s]
|
235
228
|
end
|
236
229
|
|
237
|
-
|
230
|
+
@options[:minimum] ||= @options.delete(:min)
|
231
|
+
@options[:maximum] ||= @options.delete(:max)
|
238
232
|
|
239
|
-
|
240
|
-
#
|
241
|
-
# @yield block where you should put with_tag, without_tag and/or other matchers
|
242
|
-
#
|
243
|
-
# @param [String] tag css selector for tag you want to match, e.g. 'div', 'section#my', 'article.red'
|
244
|
-
# @param [Hash] options options hash(see below)
|
245
|
-
# @option options [Hash] :with hash with html attributes, within this, *:class* option have special meaning, you may specify it as array of expected classes or string of classes separated by spaces, order does not matter
|
246
|
-
# @option options [Fixnum] :count for tag count matching(*ATTENTION:* do not use :count with :minimum(:min) or :maximum(:max))
|
247
|
-
# @option options [Range] :count not strict tag count matching, count of tags should be in specified range
|
248
|
-
# @option options [Fixnum] :minimum minimum count of elements to match
|
249
|
-
# @option options [Fixnum] :min same as :minimum
|
250
|
-
# @option options [Fixnum] :maximum maximum count of elements to match
|
251
|
-
# @option options [Fixnum] :max same as :maximum
|
252
|
-
# @option options [String/Regexp] :text to match tag content, could be either String or Regexp
|
253
|
-
#
|
254
|
-
# @example
|
255
|
-
# expect(rendered).to have_tag('div')
|
256
|
-
# expect(rendered).to have_tag('h1.header')
|
257
|
-
# expect(rendered).to have_tag('div#footer')
|
258
|
-
# expect(rendered).to have_tag('input#email', :with => { :name => 'user[email]', :type => 'email' } )
|
259
|
-
# expect(rendered).to have_tag('div', :count => 3) # matches exactly 3 'div' tags
|
260
|
-
# expect(rendered).to have_tag('div', :count => 3..7) # shortcut for have_tag('div', :minimum => 3, :maximum => 7)
|
261
|
-
# expect(rendered).to have_tag('div', :minimum => 3) # matches more(or equal) than 3 'div' tags
|
262
|
-
# expect(rendered).to have_tag('div', :maximum => 3) # matches less(or equal) than 3 'div' tags
|
263
|
-
# expect(rendered).to have_tag('p', :text => 'some content') # will match "<p>some content</p>"
|
264
|
-
# expect(rendered).to have_tag('p', :text => /some content/i) # will match "<p>sOme cOntEnt</p>"
|
265
|
-
# expect(rendered).to have_tag('textarea', :with => {:name => 'user[description]'}, :text => "I like pie")
|
266
|
-
# expect("<html>
|
267
|
-
# <body>
|
268
|
-
# <h1>some html document</h1>
|
269
|
-
# </body>
|
270
|
-
# </html>").to have_tag('body') { with_tag('h1', :text => 'some html document') }
|
271
|
-
# expect('<div class="one two">').to have_tag('div', :with => { :class => ['two', 'one'] })
|
272
|
-
# expect('<div class="one two">').to have_tag('div', :with => { :class => 'two one' })
|
273
|
-
def have_tag tag, options={}, &block
|
274
|
-
# for backwards compatibility with rpecs have tag:
|
275
|
-
options = { :text => options } if options.kind_of?(String) || options.kind_of?(Regexp)
|
276
|
-
@__current_scope_for_nokogiri_matcher = HaveTag.new(tag, options, &block)
|
233
|
+
@options[:text] = @options[:text].to_s if @options.has_key?(:text) && !@options[:text].is_a?(Regexp)
|
277
234
|
end
|
278
235
|
|
279
|
-
|
280
|
-
raise StandardError, 'this matcher should be used inside "have_tag" matcher block' unless defined?(@__current_scope_for_nokogiri_matcher)
|
281
|
-
raise ArgumentError, 'this matcher does not accept block' if block_given?
|
282
|
-
tag = @__current_scope_for_nokogiri_matcher.instance_variable_get(:@tag)
|
283
|
-
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, :text => text)
|
284
|
-
end
|
236
|
+
end
|
285
237
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
238
|
+
# tag assertion, this is the core of functionality, other matchers are shortcuts to this matcher
|
239
|
+
#
|
240
|
+
# @yield block where you should put with_tag, without_tag and/or other matchers
|
241
|
+
#
|
242
|
+
# @param [String] tag css selector for tag you want to match, e.g. 'div', 'section#my', 'article.red'
|
243
|
+
# @param [Hash] options options hash(see below)
|
244
|
+
# @option options [Hash] :with hash with html attributes, within this, *:class* option have special meaning, you may specify it as array of expected classes or string of classes separated by spaces, order does not matter
|
245
|
+
# @option options [Fixnum] :count for tag count matching(*ATTENTION:* do not use :count with :minimum(:min) or :maximum(:max))
|
246
|
+
# @option options [Range] :count not strict tag count matching, count of tags should be in specified range
|
247
|
+
# @option options [Fixnum] :minimum minimum count of elements to match
|
248
|
+
# @option options [Fixnum] :min same as :minimum
|
249
|
+
# @option options [Fixnum] :maximum maximum count of elements to match
|
250
|
+
# @option options [Fixnum] :max same as :maximum
|
251
|
+
# @option options [String/Regexp] :text to match tag content, could be either String or Regexp
|
252
|
+
#
|
253
|
+
# @example
|
254
|
+
# expect(rendered).to have_tag('div')
|
255
|
+
# expect(rendered).to have_tag('h1.header')
|
256
|
+
# expect(rendered).to have_tag('div#footer')
|
257
|
+
# expect(rendered).to have_tag('input#email', :with => { :name => 'user[email]', :type => 'email' } )
|
258
|
+
# expect(rendered).to have_tag('div', :count => 3) # matches exactly 3 'div' tags
|
259
|
+
# expect(rendered).to have_tag('div', :count => 3..7) # shortcut for have_tag('div', :minimum => 3, :maximum => 7)
|
260
|
+
# expect(rendered).to have_tag('div', :minimum => 3) # matches more(or equal) than 3 'div' tags
|
261
|
+
# expect(rendered).to have_tag('div', :maximum => 3) # matches less(or equal) than 3 'div' tags
|
262
|
+
# expect(rendered).to have_tag('p', :text => 'some content') # will match "<p>some content</p>"
|
263
|
+
# expect(rendered).to have_tag('p', :text => /some content/i) # will match "<p>sOme cOntEnt</p>"
|
264
|
+
# expect(rendered).to have_tag('textarea', :with => {:name => 'user[description]'}, :text => "I like pie")
|
265
|
+
# expect("<html>
|
266
|
+
# <body>
|
267
|
+
# <h1>some html document</h1>
|
268
|
+
# </body>
|
269
|
+
# </html>").to have_tag('body') { with_tag('h1', :text => 'some html document') }
|
270
|
+
# expect('<div class="one two">').to have_tag('div', :with => { :class => ['two', 'one'] })
|
271
|
+
# expect('<div class="one two">').to have_tag('div', :with => { :class => 'two one' })
|
272
|
+
def have_tag tag, options={}, &block
|
273
|
+
# for backwards compatibility with rpecs have tag:
|
274
|
+
options = { :text => options } if options.kind_of?(String) || options.kind_of?(Regexp)
|
275
|
+
@__current_scope_for_nokogiri_matcher = HaveTag.new(tag, options, &block)
|
276
|
+
end
|
301
277
|
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
end
|
278
|
+
def with_text text
|
279
|
+
raise StandardError, 'this matcher should be used inside "have_tag" matcher block' unless defined?(@__current_scope_for_nokogiri_matcher)
|
280
|
+
raise ArgumentError, 'this matcher does not accept block' if block_given?
|
281
|
+
tag = @__current_scope_for_nokogiri_matcher.instance_variable_get(:@tag)
|
282
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, :text => text)
|
283
|
+
end
|
309
284
|
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
285
|
+
def without_text text
|
286
|
+
raise StandardError, 'this matcher should be used inside "have_tag" matcher block' unless defined?(@__current_scope_for_nokogiri_matcher)
|
287
|
+
raise ArgumentError, 'this matcher does not accept block' if block_given?
|
288
|
+
tag = @__current_scope_for_nokogiri_matcher.instance_variable_get(:@tag)
|
289
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, :text => text)
|
290
|
+
end
|
291
|
+
alias :but_without_text :without_text
|
292
|
+
|
293
|
+
# with_tag matcher
|
294
|
+
# @yield block where you should put other with_tag or without_tag
|
295
|
+
# @see #have_tag
|
296
|
+
# @note this should be used within block of have_tag matcher
|
297
|
+
def with_tag tag, options={}, &block
|
298
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, options, &block)
|
299
|
+
end
|
324
300
|
|
325
|
-
|
301
|
+
# without_tag matcher
|
302
|
+
# @yield block where you should put other with_tag or without_tag
|
303
|
+
# @see #have_tag
|
304
|
+
# @note this should be used within block of have_tag matcher
|
305
|
+
def without_tag tag, options={}, &block
|
306
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, options, &block)
|
307
|
+
end
|
326
308
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
309
|
+
# form assertion
|
310
|
+
#
|
311
|
+
# it is a shortcut to
|
312
|
+
# have_tag 'form', :with => { :action => action_url, :method => method ... }
|
313
|
+
# @yield block with with_<field>, see below
|
314
|
+
# @see #have_tag
|
315
|
+
def have_form action_url, method, options={}, &block
|
316
|
+
options[:with] ||= {}
|
317
|
+
id = options[:with].delete(:id)
|
318
|
+
tag = 'form'; tag << '#'+id if id
|
319
|
+
options[:with].merge!(:action => action_url)
|
320
|
+
options[:with].merge!(:method => method.to_s)
|
321
|
+
have_tag tag, options, &block
|
322
|
+
end
|
331
323
|
|
332
|
-
|
333
|
-
options = form_tag_options('hidden',name,value)
|
334
|
-
should_not_have_input(options)
|
335
|
-
end
|
324
|
+
#TODO fix code duplications
|
336
325
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
326
|
+
def with_hidden_field name, value=nil
|
327
|
+
options = form_tag_options('hidden',name,value)
|
328
|
+
should_have_input(options)
|
329
|
+
end
|
341
330
|
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
331
|
+
def without_hidden_field name, value=nil
|
332
|
+
options = form_tag_options('hidden',name,value)
|
333
|
+
should_not_have_input(options)
|
334
|
+
end
|
346
335
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
336
|
+
def with_text_field name, value=nil
|
337
|
+
options = form_tag_options('text',name,value)
|
338
|
+
should_have_input(options)
|
339
|
+
end
|
351
340
|
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
341
|
+
def without_text_field name, value=nil
|
342
|
+
options = form_tag_options('text',name,value)
|
343
|
+
should_not_have_input(options)
|
344
|
+
end
|
356
345
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
346
|
+
def with_email_field name, value=nil
|
347
|
+
options = form_tag_options('email',name,value)
|
348
|
+
should_have_input(options)
|
349
|
+
end
|
361
350
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
351
|
+
def without_email_field name, value=nil
|
352
|
+
options = form_tag_options('email',name,value)
|
353
|
+
should_not_have_input(options)
|
354
|
+
end
|
366
355
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
356
|
+
def with_url_field name, value=nil
|
357
|
+
options = form_tag_options('url',name,value)
|
358
|
+
should_have_input(options)
|
359
|
+
end
|
371
360
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
361
|
+
def without_url_field name, value=nil
|
362
|
+
options = form_tag_options('url',name,value)
|
363
|
+
should_not_have_input(options)
|
364
|
+
end
|
376
365
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
366
|
+
def with_number_field name, value=nil
|
367
|
+
options = form_tag_options('number',name,value)
|
368
|
+
should_have_input(options)
|
369
|
+
end
|
381
370
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
should_not_have_input(options)
|
387
|
-
end
|
371
|
+
def without_number_field name, value=nil
|
372
|
+
options = form_tag_options('number',name,value)
|
373
|
+
should_not_have_input(options)
|
374
|
+
end
|
388
375
|
|
389
|
-
|
376
|
+
def with_range_field name, min, max, options={}
|
377
|
+
options = { :with => { :name => name, :type => 'range', :min => min.to_s, :max => max.to_s }.merge(options.delete(:with)||{}) }
|
378
|
+
should_have_input(options)
|
379
|
+
end
|
390
380
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
end
|
381
|
+
def without_range_field name, min=nil, max=nil, options={}
|
382
|
+
options = { :with => { :name => name, :type => 'range' }.merge(options.delete(:with)||{}) }
|
383
|
+
options[:with].merge!(:min => min.to_s) if min
|
384
|
+
options[:with].merge!(:max => max.to_s) if max
|
385
|
+
should_not_have_input(options)
|
386
|
+
end
|
398
387
|
|
399
|
-
|
400
|
-
date_field_type = date_field_type.to_s
|
401
|
-
raise "unknown type `#{date_field_type}` for date picker" unless DATE_FIELD_TYPES.include?(date_field_type)
|
402
|
-
options = { :with => { :type => date_field_type.to_s }.merge(options.delete(:with)||{}) }
|
403
|
-
options[:with].merge!(:name => name.to_s) if name
|
404
|
-
should_not_have_input(options)
|
405
|
-
end
|
388
|
+
DATE_FIELD_TYPES = %w( date month week time datetime datetime-local )
|
406
389
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
390
|
+
def with_date_field date_field_type, name=nil, options={}
|
391
|
+
date_field_type = date_field_type.to_s
|
392
|
+
raise "unknown type `#{date_field_type}` for date picker" unless DATE_FIELD_TYPES.include?(date_field_type)
|
393
|
+
options = { :with => { :type => date_field_type.to_s }.merge(options.delete(:with)||{}) }
|
394
|
+
options[:with].merge!(:name => name.to_s) if name
|
395
|
+
should_have_input(options)
|
396
|
+
end
|
412
397
|
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
398
|
+
def without_date_field date_field_type, name=nil, options={}
|
399
|
+
date_field_type = date_field_type.to_s
|
400
|
+
raise "unknown type `#{date_field_type}` for date picker" unless DATE_FIELD_TYPES.include?(date_field_type)
|
401
|
+
options = { :with => { :type => date_field_type.to_s }.merge(options.delete(:with)||{}) }
|
402
|
+
options[:with].merge!(:name => name.to_s) if name
|
403
|
+
should_not_have_input(options)
|
404
|
+
end
|
417
405
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
406
|
+
# TODO add ability to explicitly say that value should be empty
|
407
|
+
def with_password_field name, value=nil
|
408
|
+
options = form_tag_options('password',name,value)
|
409
|
+
should_have_input(options)
|
410
|
+
end
|
422
411
|
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
412
|
+
def without_password_field name, value=nil
|
413
|
+
options = form_tag_options('password',name,value)
|
414
|
+
should_not_have_input(options)
|
415
|
+
end
|
427
416
|
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
end
|
417
|
+
def with_file_field name, value=nil
|
418
|
+
options = form_tag_options('file',name,value)
|
419
|
+
should_have_input(options)
|
420
|
+
end
|
433
421
|
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
end
|
422
|
+
def without_file_field name, value=nil
|
423
|
+
options = form_tag_options('file',name,value)
|
424
|
+
should_not_have_input(options)
|
425
|
+
end
|
439
426
|
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
427
|
+
def with_text_area name#TODO, text=nil
|
428
|
+
#options = form_tag_options('text',name,value)
|
429
|
+
options = { :with => { :name => name } }
|
430
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag('textarea', options)
|
431
|
+
end
|
444
432
|
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
433
|
+
def without_text_area name#TODO, text=nil
|
434
|
+
#options = form_tag_options('text',name,value)
|
435
|
+
options = { :with => { :name => name } }
|
436
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag('textarea', options)
|
437
|
+
end
|
449
438
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
439
|
+
def with_checkbox name, value=nil
|
440
|
+
options = form_tag_options('checkbox',name,value)
|
441
|
+
should_have_input(options)
|
442
|
+
end
|
454
443
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
444
|
+
def without_checkbox name, value=nil
|
445
|
+
options = form_tag_options('checkbox',name,value)
|
446
|
+
should_not_have_input(options)
|
447
|
+
end
|
459
448
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
options[:with].merge!(:name => name)
|
465
|
-
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, options, &block)
|
466
|
-
end
|
449
|
+
def with_radio_button name, value
|
450
|
+
options = form_tag_options('radio',name,value)
|
451
|
+
should_have_input(options)
|
452
|
+
end
|
467
453
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
options[:with].merge!(:name => name)
|
473
|
-
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, options, &block)
|
474
|
-
end
|
454
|
+
def without_radio_button name, value
|
455
|
+
options = form_tag_options('radio',name,value)
|
456
|
+
should_not_have_input(options)
|
457
|
+
end
|
475
458
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
options[:with].merge!(:value => value.to_s) if value
|
484
|
-
if options[:selected]
|
485
|
-
options[:with].merge!(:selected => "selected")
|
486
|
-
end
|
487
|
-
options.delete(:selected)
|
488
|
-
options.merge!(:text => text) if text
|
489
|
-
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, options)
|
490
|
-
end
|
459
|
+
def with_select name, options={}, &block
|
460
|
+
options[:with] ||= {}
|
461
|
+
id = options[:with].delete(:id)
|
462
|
+
tag='select'; tag << '#'+id if id
|
463
|
+
options[:with].merge!(:name => name)
|
464
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, options, &block)
|
465
|
+
end
|
491
466
|
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
options[:with].merge!(:value => value.to_s) if value
|
500
|
-
if options[:selected]
|
501
|
-
options[:with].merge!(:selected => "selected")
|
502
|
-
end
|
503
|
-
options.delete(:selected)
|
504
|
-
options.merge!(:text => text) if text
|
505
|
-
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, options)
|
506
|
-
end
|
467
|
+
def without_select name, options={}, &block
|
468
|
+
options[:with] ||= {}
|
469
|
+
id = options[:with].delete(:id)
|
470
|
+
tag='select'; tag << '#'+id if id
|
471
|
+
options[:with].merge!(:name => name)
|
472
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, options, &block)
|
473
|
+
end
|
507
474
|
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
475
|
+
def with_option text, value=nil, options={}
|
476
|
+
options[:with] ||= {}
|
477
|
+
if value.is_a?(Hash)
|
478
|
+
options.merge!(value)
|
479
|
+
value=nil
|
480
|
+
end
|
481
|
+
tag='option'
|
482
|
+
options[:with].merge!(:value => value.to_s) if value
|
483
|
+
if options[:selected]
|
484
|
+
options[:with].merge!(:selected => "selected")
|
517
485
|
end
|
486
|
+
options.delete(:selected)
|
487
|
+
options.merge!(:text => text) if text
|
488
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag(tag, options)
|
489
|
+
end
|
518
490
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
491
|
+
def without_option text, value=nil, options={}
|
492
|
+
options[:with] ||= {}
|
493
|
+
if value.is_a?(Hash)
|
494
|
+
options.merge!(value)
|
495
|
+
value=nil
|
496
|
+
end
|
497
|
+
tag='option'
|
498
|
+
options[:with].merge!(:value => value.to_s) if value
|
499
|
+
if options[:selected]
|
500
|
+
options[:with].merge!(:selected => "selected")
|
528
501
|
end
|
502
|
+
options.delete(:selected)
|
503
|
+
options.merge!(:text => text) if text
|
504
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag(tag, options)
|
505
|
+
end
|
529
506
|
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
507
|
+
def with_button text, value=nil, options={}
|
508
|
+
options[:with] ||= {}
|
509
|
+
if value.is_a?(Hash)
|
510
|
+
options.merge!(value)
|
511
|
+
value=nil
|
534
512
|
end
|
513
|
+
options[:with].merge!(:value => value.to_s) if value
|
514
|
+
options.merge!(:text => text) if text
|
515
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag('button', options)
|
516
|
+
end
|
535
517
|
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
518
|
+
def without_button text, value=nil, options={}
|
519
|
+
options[:with] ||= {}
|
520
|
+
if value.is_a?(Hash)
|
521
|
+
options.merge!(value)
|
522
|
+
value=nil
|
540
523
|
end
|
524
|
+
options[:with].merge!(:value => value.to_s) if value
|
525
|
+
options.merge!(:text => text) if text
|
526
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag('button', options)
|
527
|
+
end
|
541
528
|
|
542
|
-
|
529
|
+
def with_submit value
|
530
|
+
options = { :with => { :type => 'submit', :value => value } }
|
531
|
+
#options = form_tag_options('text',name,value)
|
532
|
+
should_have_input(options)
|
533
|
+
end
|
543
534
|
|
544
|
-
|
545
|
-
|
546
|
-
|
535
|
+
def without_submit value
|
536
|
+
#options = form_tag_options('text',name,value)
|
537
|
+
options = { :with => { :type => 'submit', :value => value } }
|
538
|
+
should_not_have_input(options)
|
539
|
+
end
|
547
540
|
|
548
|
-
|
549
|
-
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag('input', options)
|
550
|
-
end
|
541
|
+
private
|
551
542
|
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
# .to_s if value is a digit or smth. else, see issue#10
|
556
|
-
options[:with].merge!(:value => form_tag_value.to_s) if form_tag_value
|
557
|
-
return options
|
558
|
-
end
|
543
|
+
def should_have_input(options)
|
544
|
+
expect(@__current_scope_for_nokogiri_matcher).to have_tag('input', options)
|
545
|
+
end
|
559
546
|
|
547
|
+
def should_not_have_input(options)
|
548
|
+
expect(@__current_scope_for_nokogiri_matcher).to_not have_tag('input', options)
|
560
549
|
end
|
561
|
-
end
|
562
550
|
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
551
|
+
# form_tag in method name name mean smth. like input, submit, tags that should appear in a form
|
552
|
+
def form_tag_options form_tag_type, form_tag_name, form_tag_value=nil
|
553
|
+
options = { :with => { :name => form_tag_name, :type => form_tag_type } }
|
554
|
+
# .to_s if value is a digit or smth. else, see issue#10
|
555
|
+
options[:with].merge!(:value => form_tag_value.to_s) if form_tag_value
|
556
|
+
return options
|
568
557
|
end
|
558
|
+
|
569
559
|
end
|