rspec_hpricot_matchers 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Kyle Hargraves
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND
17
+ NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,71 @@
1
+ = rspec_hpricot_matchers
2
+
3
+ An implementation of have_tag(), as in rspec_on_rails, but sitting atop
4
+ Hpricot rather than merely wrapping assert_select().
5
+
6
+
7
+ == Usage
8
+
9
+ As its first argument, have_tag() accepts any CSS or XPath selectors
10
+ which are supported by Hpricot.
11
+
12
+ body.should have_tag('form[@action*=session]')
13
+ body.should have_tag('ul > li + li')
14
+
15
+ Expectations can be placed upon the inner text of the matched element
16
+ by providing another argument, which should be either a String or a
17
+ Regexp:
18
+
19
+ body.should have_tag('h1', 'Welcome')
20
+ body.should have_tag('p', /a very important blurb/i)
21
+
22
+ Expectations can be placed upon the number of matched elements by
23
+ passing an options hash:
24
+
25
+ body.should have_tag('abbr', :count => 1) # exactly one
26
+ body.should have_tag('dt', :minimum => 4) # at least 4
27
+ body.should have_tag('dd', :maximum => 4) # at most 4
28
+ body.should have_tag('a.outgoing', /rspec/i, :count => 2)
29
+
30
+ The :count key also accepts a Range, making the following equivalent:
31
+
32
+ body.should have_tag('tr', :count => 3..5)
33
+ body.should have_tag('tr', :minimum => 3,
34
+ :maximum => 5)
35
+
36
+
37
+ The usage of with_tag(), however, is no longer supported. Instead, a
38
+ block passed to have_tag() will have each matched element successively
39
+ yielded to it. If none of the blocks return without raising an
40
+ ExpectationNotMetError, the outer have_tag() is treated as having failed:
41
+
42
+ body.should have_tag('thead') do |thead|
43
+ thead.should have_tag('th', :count => 5)
44
+ end
45
+
46
+ This also allows arbitrary expectations to be applied from within
47
+ the block, such as:
48
+
49
+ body.should have_tag('dl dd.sha1') do |dd|
50
+ dd.inner_text.length.should == 40
51
+ end
52
+
53
+
54
+ == Notes
55
+
56
+ Currently, this implementation does not support substitution values
57
+ as assert_select did (by way of HTML::Selector):
58
+
59
+ # Not yet supported:
60
+ body.should have_tag('li[class=?]', dom_class)
61
+ body.should have_tag('tr.person#?', /^person-\d+$/)
62
+
63
+ I personally rarely use these, and Hpricot's advanced selectors make
64
+ them mostly useless, as far as I can tell, so I am unlikely to
65
+ implement them myself.
66
+
67
+ This have_tag() further differs from the assert_select-based
68
+ implementation in that the nested have_tag() calls must *all* pass
69
+ on a single selected element in order to be true. This was a source
70
+ of confusion in RSpec ticket #316. There is a spec covering this
71
+ case if you need an example.
@@ -0,0 +1,27 @@
1
+ require 'rake/gempackagetask'
2
+ require 'spec/rake/spectask'
3
+
4
+ task :default => :spec
5
+ Spec::Rake::SpecTask.new
6
+
7
+ gemspec = Gem::Specification.new do |spec|
8
+ spec.name = 'rspec_hpricot_matchers'
9
+ spec.summary = "Implementation of have_tag() rspec matcher using Hpricot"
10
+ spec.version = '1.0'
11
+ spec.author = 'Kyle Hargraves'
12
+ spec.email = 'philodespotos@gmail.com'
13
+ spec.description = <<-END
14
+ rspec_hpricot_matchers provides an implementation of rspec_on_rails'
15
+ have_tag() matcher which does not depend on Rails' assert_select.
16
+ Using Hpricot instead, the matcher is available to non-Rails projects,
17
+ and enjoys the full flexibility of Hpricot's advanced CSS and XPath
18
+ selector support.
19
+ END
20
+ spec.files = FileList['lib/**/*', 'spec/**/*', 'README', 'MIT-LICENSE', 'Rakefile']
21
+
22
+ spec.rubyforge_project = 'rspec-hpricot'
23
+ spec.homepage = 'http://rspec-hpricot.rubyforge.org'
24
+ end
25
+
26
+ Rake::GemPackageTask.new(gemspec) do |spec|
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'hpricot'
3
+ require 'rspec_hpricot_matchers/have_tag'
4
+
5
+ # evil hack to duck-type CgiResponse so that nested shoulds can use
6
+ # +rspec_on_rails+ matchers without remembering to call to_s on it
7
+ #
8
+ # e.g.
9
+ #
10
+ # response.should have_tag("li") do |ul|
11
+ # ul.should have_text("List Item") # with hack
12
+ # ul.to_s.should have_text("List Item") # without hack
13
+ # end
14
+ class Hpricot::Elem
15
+ alias body to_s
16
+ end
@@ -0,0 +1,112 @@
1
+ module RspecHpricotMatchers
2
+ class HaveTag
3
+ def initialize(selector, inner_text_or_options, options, &block)
4
+ @selector = selector
5
+ if Hash === inner_text_or_options
6
+ @inner_text = nil
7
+ @options = inner_text_or_options
8
+ else
9
+ @inner_text = inner_text_or_options
10
+ @options = options
11
+ end
12
+ end
13
+
14
+ def matches?(actual, &block)
15
+ @actual = actual
16
+ @hdoc = hdoc_for(@actual)
17
+
18
+ matched_elements = @hdoc.search(@selector)
19
+ if matched_elements.empty?
20
+ return @options[:count] == 0
21
+ end
22
+
23
+ if @inner_text
24
+ matched_elements = filter_on_inner_text(matched_elements)
25
+ end
26
+
27
+ if block
28
+ matched_elements = filter_on_nested_expectations(matched_elements, block)
29
+ end
30
+
31
+ @actual_count = matched_elements.length
32
+ return false if not acceptable_count?(@actual_count)
33
+
34
+ !matched_elements.empty?
35
+ end
36
+
37
+ def failure_message
38
+ explanation = @actual_count ? "but found #{@actual_count}" : "but did not"
39
+ "expected\n#{@hdoc.to_s}\nto have #{failure_count_phrase} #{failure_selector_phrase}, #{explanation}"
40
+ end
41
+
42
+ def negative_failure_message
43
+ explanation = @actual_count ? "but found #{@actual_count}" : "but did"
44
+ "expected\n#{@hdoc.to_s}\nnot to have #{failure_count_phrase} #{failure_selector_phrase}, #{explanation}"
45
+ end
46
+
47
+ private
48
+ def hdoc_for(input)
49
+ if Hpricot === input
50
+ input
51
+ elsif input.respond_to?(:body)
52
+ Hpricot(input.body)
53
+ else
54
+ Hpricot(input.to_s)
55
+ end
56
+ end
57
+
58
+ def filter_on_inner_text(elements)
59
+ elements.select do |el|
60
+ next(el.inner_text =~ @inner_text) if @inner_text.is_a?(Regexp)
61
+ el.inner_text == @inner_text
62
+ end
63
+ end
64
+
65
+ def filter_on_nested_expectations(elements, block)
66
+ elements.select do |el|
67
+ begin
68
+ block.call(el)
69
+ rescue Spec::Expectations::ExpectationNotMetError
70
+ false
71
+ else
72
+ true
73
+ end
74
+ end
75
+ end
76
+
77
+ def acceptable_count?(actual_count)
78
+ if @options[:count]
79
+ return false unless @options[:count] === actual_count
80
+ end
81
+ if @options[:minimum]
82
+ return false unless actual_count >= @options[:minimum]
83
+ end
84
+ if @options[:maximum]
85
+ return false unless actual_count <= @options[:maximum]
86
+ end
87
+ true
88
+ end
89
+
90
+ def failure_count_phrase
91
+ if @options[:count]
92
+ "#{@options[:count]} elements matching"
93
+ elsif @options[:minimum] || @options[:maximum]
94
+ count_explanations = []
95
+ count_explanations << "at least #{@options[:minimum]}" if @options[:minimum]
96
+ count_explanations << "at most #{@options[:maximum]}" if @options[:maximum]
97
+ "#{count_explanations.join(' and ')} elements matching"
98
+ else
99
+ "an element matching"
100
+ end
101
+ end
102
+
103
+ def failure_selector_phrase
104
+ phrase = @selector.inspect
105
+ phrase << (@inner_text ? " with inner text #{@inner_text.inspect}" : "")
106
+ end
107
+ end
108
+
109
+ def have_tag(selector, inner_text_or_options = nil, options = {}, &block)
110
+ HaveTag.new(selector, inner_text_or_options, options, &block)
111
+ end
112
+ end
@@ -0,0 +1,244 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ unless defined?(SpecFailed)
4
+ SpecFailed = Spec::Expectations::ExpectationNotMetError
5
+ end
6
+
7
+ describe 'have_tag' do
8
+ before(:each) do
9
+ @html = "<ul><li>An egregiously long string</li></ul>"
10
+ end
11
+
12
+ it "should match against strings" do
13
+ @html.should have_tag('li')
14
+ end
15
+
16
+ it "should match against Hpricot documents" do
17
+ hdoc = Hpricot(@html)
18
+ hdoc.should have_tag('li')
19
+ end
20
+
21
+ it "should use the response of #body if the target responds to it" do
22
+ response = Object.new
23
+ class << response
24
+ def body
25
+ "<ul><li>An egregiously long string</li></ul>"
26
+ end
27
+ end
28
+ response.should have_tag('li')
29
+ end
30
+
31
+ it "should not match when the target does not have the selected element" do
32
+ @html.should_not have_tag('dd')
33
+ end
34
+
35
+ it "should match against the inner text of the selected element" do
36
+ @html.should have_tag('li', 'An egregiously long string')
37
+ end
38
+
39
+ it "should match negatively against the inner text" do
40
+ @html.should_not have_tag('li', 'Some other string entirely')
41
+ end
42
+
43
+ it "should match against a Regexp describing the inner text" do
44
+ @html.should have_tag('li', /GREG/i)
45
+ end
46
+
47
+ it "should match negatively against a Regexp describing the inner text" do
48
+ @html.should_not have_tag('li', /GREG/)
49
+ end
50
+
51
+ it "should include the body in the failure message" do
52
+ lambda {
53
+ @html.should have_tag('abbr')
54
+ }.should raise_error(SpecFailed, /#{Regexp.escape(@html)}/)
55
+ end
56
+
57
+ it "should include the selector in the failure message" do
58
+ lambda {
59
+ @html.should have_tag('abbr')
60
+ }.should raise_error(SpecFailed, /"abbr"/)
61
+ end
62
+
63
+ it "should include the expected inner text if provided" do
64
+ lambda {
65
+ @html.should have_tag('li', /something else/)
66
+ }.should raise_error(SpecFailed, %r{/something else/})
67
+ end
68
+ end
69
+
70
+ describe 'have_tag inner expectations' do
71
+ before(:each) do
72
+ @html = "<ul><li>An egregiously long string</li></ul>"
73
+ end
74
+
75
+ it "should fail when the outer selector fails" do
76
+ lambda {
77
+ @html.should have_tag('dl') do |dl|
78
+ dl.should have_tag('li')
79
+ end
80
+ }.should raise_error(SpecFailed)
81
+ end
82
+
83
+ it "should match the inner expectations against the elements matched by the outer selector" do
84
+ @html.should have_tag('ul') do |ul|
85
+ ul.should have_tag('li')
86
+ end
87
+ end
88
+
89
+ it "should support negated inner expectations" do
90
+ @html.should have_tag('ul') do |ul|
91
+ ul.should_not have_tag('dd')
92
+ end
93
+ end
94
+
95
+ it "should treat multiple nested have_tag() expectations as a logical AND" do
96
+ @html.should have_tag('ul') do |ul|
97
+ ul.should have_tag('li')
98
+ ul.should_not have_tag('dd')
99
+ end
100
+ end
101
+
102
+ it "should only match against a single element at a time when nesting expectations (see RSpec LH#316)" do
103
+ html = <<-EOHTML
104
+ <ul>
105
+ <li>
106
+ <a href="1">1</a>
107
+ </li>
108
+ <li>
109
+ <a href="2">2</a>
110
+ <span>Hello</span>
111
+ </li>
112
+ </ul>
113
+ EOHTML
114
+
115
+ html.should have_tag('li', :count => 1) do |li|
116
+ li.should have_tag('a')
117
+ li.should have_tag('span')
118
+ end
119
+
120
+ html.should have_tag('li', :count => 1) do |li|
121
+ li.should have_tag('a')
122
+ li.should_not have_tag('span')
123
+ end
124
+
125
+ html.should have_tag('li', :count => 2) do |li|
126
+ li.should have_tag('a')
127
+ end
128
+ end
129
+
130
+ it "should yield elements which respond to #body" do
131
+ @html.should have_tag('ul') do |ul|
132
+ ul.should respond_to(:body)
133
+ end
134
+ end
135
+
136
+ it "should supports arbitrary expectations within the block" do
137
+ html = %q{<span class="sha1">cbc0bd52f99fe19304bccad383694e92b8ee2c71</span>}
138
+ html.should have_tag('span.sha1') do |span|
139
+ span.inner_text.length.should == 40
140
+ end
141
+ html.should_not have_tag('span.sha1') do |span|
142
+ span.inner_text.length.should == 41
143
+ end
144
+ end
145
+
146
+ it "should include a description of the inner expectation in the failure message" do
147
+ pending "No idea how to implement this."
148
+ end
149
+ end
150
+
151
+ describe 'have_tag with counts' do
152
+ before(:each) do
153
+ @html = <<-EOHTML
154
+ <ul>
155
+ <li>Foo</li>
156
+ <li>Bar</li>
157
+ <li>Foo again</li>
158
+ <li><a href="/baz">With inner elements</a></li>
159
+ </ul>
160
+ EOHTML
161
+ end
162
+
163
+ it "should treat an integer :count as expecting exactly n matched elements" do
164
+ @html.should have_tag('li', :count => 4)
165
+ @html.should_not have_tag('li', :count => 3)
166
+ @html.should_not have_tag('li', :count => 5)
167
+ end
168
+
169
+ it "should treat a range :count as expecting between x and y matched elements" do
170
+ @html.should have_tag('li', :count => 1..5)
171
+ @html.should_not have_tag('li', :count => 2..3)
172
+ end
173
+
174
+ it "should treat a :count of zero as if a negative match were expected" do
175
+ @html.should have_tag('dd', :count => 0)
176
+ end
177
+
178
+ it "should treat :minimum as expecting at least n matched elements" do
179
+ (0..4).each { |n| @html.should have_tag('li', :minimum => n) }
180
+ @html.should_not have_tag('li', :minimum => 5)
181
+ end
182
+
183
+ it "should treat :maximum as expecting at most n matched elements" do
184
+ @html.should_not have_tag('li', :maximum => 3)
185
+ @html.should have_tag('li', :maximum => 4)
186
+ @html.should have_tag('li', :maximum => 5)
187
+ end
188
+
189
+ it "should support matching of content while specifying a count" do
190
+ @html.should have_tag('li', /foo/i, :count => 2)
191
+ end
192
+
193
+ it "should work when the have_tag is nested" do
194
+ @html.should have_tag('ul') do |ul|
195
+ ul.should have_tag('li', :minimum => 2)
196
+ ul.should have_tag('li', /foo/i, :count => 2)
197
+ end
198
+ end
199
+
200
+ it "should include the actual number of elements matched in the failure message" do
201
+ lambda {
202
+ @html.should have_tag('li', :count => 3)
203
+ }.should raise_error(SpecFailed, /found 4/)
204
+ lambda {
205
+ @html.should have_tag('li', :count => 5)
206
+ }.should raise_error(SpecFailed, /found 4/)
207
+ end
208
+
209
+ it "should include the actual number of elements matched in the negative failure message" do
210
+ lambda {
211
+ @html.should_not have_tag('li', :count => 4)
212
+ }.should raise_error(SpecFailed, /found 4/)
213
+ end
214
+
215
+ it "should include the expected number of elements in the failure message" do
216
+ lambda {
217
+ @html.should have_tag('li', :count => 2)
218
+ }.should raise_error(SpecFailed, /to have 2 elements matching/)
219
+ end
220
+
221
+ it "should include the expected number of elements in the negative failure message" do
222
+ lambda {
223
+ @html.should_not have_tag('li', :count => 4)
224
+ }.should raise_error(SpecFailed, /to have 4 elements matching/)
225
+ end
226
+
227
+ it "should describe the :minimum case using 'at least ...'" do
228
+ lambda {
229
+ @html.should have_tag('li', :minimum => 80)
230
+ }.should raise_error(SpecFailed, /to have at least 80 elements matching/)
231
+ end
232
+
233
+ it "should describe the :maximum case using 'at most ...'" do
234
+ lambda {
235
+ @html.should have_tag('li', :maximum => 2)
236
+ }.should raise_error(SpecFailed, /to have at most 2 elements matching/)
237
+ end
238
+
239
+ it "should describe the :minimum and :maximum case using 'at least ... and at most ...'" do
240
+ lambda {
241
+ @html.should have_tag('li', :minimum => 8, :maximum => 30)
242
+ }.should raise_error(SpecFailed, /to have at least 8 and at most 30 elements matching/)
243
+ end
244
+ end
@@ -0,0 +1,6 @@
1
+ $: << File.dirname(__FILE__) + '/../lib'
2
+ require 'rspec_hpricot_matchers'
3
+
4
+ Spec::Runner.configure do |config|
5
+ config.include(RspecHpricotMatchers)
6
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec_hpricot_matchers
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Kyle Hargraves
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-03-26 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: rspec_hpricot_matchers provides an implementation of rspec_on_rails' have_tag() matcher which does not depend on Rails' assert_select. Using Hpricot instead, the matcher is available to non-Rails projects, and enjoys the full flexibility of Hpricot's advanced CSS and XPath selector support.
17
+ email: philodespotos@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/rspec_hpricot_matchers
26
+ - lib/rspec_hpricot_matchers/have_tag.rb
27
+ - lib/rspec_hpricot_matchers.rb
28
+ - spec/rspec_hpricot_matchers
29
+ - spec/rspec_hpricot_matchers/have_tag_spec.rb
30
+ - spec/spec_helper.rb
31
+ - README
32
+ - MIT-LICENSE
33
+ - Rakefile
34
+ has_rdoc: false
35
+ homepage: http://rspec-hpricot.rubyforge.org
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project: rspec-hpricot
56
+ rubygems_version: 1.0.1
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: Implementation of have_tag() rspec matcher using Hpricot
60
+ test_files: []
61
+