assert2 0.4.6 → 0.4.7
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.
- data/lib/assert2/xhtml.rb +171 -192
- metadata +2 -2
data/lib/assert2/xhtml.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
One Yury Kotlyarov recently
|
2
|
+
One Yury Kotlyarov recently this Rails project as a question:
|
3
3
|
|
4
4
|
http://github.com/yura/howto-rspec-custom-matchers/tree/master
|
5
5
|
|
@@ -60,212 +60,206 @@ requirements:
|
|
60
60
|
|
61
61
|
require 'nokogiri'
|
62
62
|
|
63
|
-
class
|
64
|
-
|
65
|
-
class XPathYielder
|
66
|
-
def initialize(method_name, &block)
|
67
|
-
self.class.send :define_method, method_name do |*args|
|
68
|
-
raise 'must call with block' unless block
|
69
|
-
block.call(*args)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
63
|
+
class BeHtmlWith
|
73
64
|
|
74
|
-
def
|
75
|
-
|
65
|
+
def initialize(scope, &block)
|
66
|
+
@scope, @block = scope, block
|
67
|
+
@references = []
|
68
|
+
@spewed = {}
|
76
69
|
end
|
77
70
|
|
78
|
-
|
71
|
+
attr_accessor :builder,
|
72
|
+
:doc,
|
73
|
+
:failure_message,
|
74
|
+
:scope
|
75
|
+
attr_reader :references
|
76
|
+
attr_writer :reference,
|
77
|
+
:sample
|
79
78
|
|
80
|
-
|
81
|
-
|
82
|
-
def deAmpAmp(stwing)
|
83
|
-
stwing.to_s.gsub('&', '&').gsub('&', '&')
|
84
|
-
end # ERGO await a fix in Nokogiri, and hope nobody actually means & !!!
|
79
|
+
def matches?(stwing, &block)
|
80
|
+
@block = block
|
85
81
|
|
86
|
-
|
87
|
-
|
82
|
+
@scope.wrap_expectation self do
|
83
|
+
@doc = Nokogiri::HTML(stwing)
|
84
|
+
return run_all_xpaths(build_xpaths)
|
85
|
+
end
|
88
86
|
end
|
87
|
+
|
88
|
+
def build_xpaths(&block)
|
89
|
+
bwock = block || @block || proc{} # CONSIDER what to do with no block? validate?
|
90
|
+
@builder = Nokogiri::HTML::Builder.new(&bwock)
|
89
91
|
|
90
|
-
|
91
|
-
|
92
|
-
|
92
|
+
elemental_children.map do |child|
|
93
|
+
build_deep_xpath(child)
|
94
|
+
end
|
93
95
|
end
|
94
96
|
|
95
|
-
def
|
96
|
-
|
97
|
+
def elemental_children(element = @builder.doc)
|
98
|
+
element.children.grep(Nokogiri::XML::Element)
|
99
|
+
end
|
97
100
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
101
|
+
def build_deep_xpath(element)
|
102
|
+
path = build_xpath(element)
|
103
|
+
path.index('not(') == 0 and return '/*[ ' + path + ' ]'
|
104
|
+
return '//' + path
|
105
|
+
end
|
102
106
|
|
103
|
-
def
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
def build_xpath(element)
|
108
|
+
count = @references.length
|
109
|
+
@references << element # note we skip the without @reference!
|
110
|
+
|
111
|
+
if element.name == 'without!'
|
112
|
+
return 'not( ' + build_predicate(element, 'or') + ' )'
|
113
|
+
else
|
114
|
+
target = element.name.sub(/\!$/, '')
|
115
|
+
path = "descendant::#{ target }[ refer(., '#{ count }') "
|
116
|
+
# refer() is first so we collect many samples, despite boolean short-circuiting
|
117
|
+
path << 'and ' if elemental_children(element).any?
|
118
|
+
path << build_predicate(element) + ']'
|
119
|
+
return path
|
111
120
|
end
|
112
121
|
end
|
113
122
|
|
114
|
-
def
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
def
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
ref == sam or
|
127
|
-
match_regexp(ref, sam) or
|
128
|
-
match_class(attr.name, ref, sam) or
|
129
|
-
return false
|
123
|
+
def build_predicate(element, conjunction = 'and')
|
124
|
+
conjunction = " #{ conjunction } "
|
125
|
+
element_kids = elemental_children(element)
|
126
|
+
return element_kids.map{|child| build_xpath(child) }.join(conjunction)
|
127
|
+
end
|
128
|
+
|
129
|
+
def run_all_xpaths(xpaths)
|
130
|
+
xpaths.each do |path|
|
131
|
+
if match_xpath(path).empty?
|
132
|
+
complain
|
133
|
+
return false
|
130
134
|
end
|
131
135
|
end
|
132
136
|
|
133
137
|
return true
|
134
138
|
end
|
135
139
|
|
136
|
-
def match_xpath(
|
137
|
-
|
138
|
-
|
139
|
-
match_paths = matches.map{|m| m.path }
|
140
|
-
return match_paths.include? sample.path
|
140
|
+
def match_xpath(path, &refer)
|
141
|
+
@doc.root.xpath_with_callback path, :refer do |element, index|
|
142
|
+
collect_samples(element, index.to_i)
|
141
143
|
end
|
142
|
-
|
143
|
-
return true
|
144
144
|
end
|
145
145
|
|
146
|
-
|
147
|
-
if match_attributes(reference, sample) and
|
148
|
-
match_text(reference, sample) and
|
149
|
-
match_xpath(reference, sample)
|
150
|
-
|
151
|
-
verbose_spew(reference, sample)
|
152
|
-
return true
|
153
|
-
end
|
146
|
+
# ERGO match text with internal spacies?
|
154
147
|
|
155
|
-
|
148
|
+
def collect_samples(elements, index) # TODO rename these samples to specimens
|
149
|
+
samples = elements.find_all do |element|
|
150
|
+
match_attributes_and_text(@references[index], element)
|
151
|
+
end
|
152
|
+
|
153
|
+
collect_best_sample(samples)
|
154
|
+
samples
|
156
155
|
end
|
157
156
|
|
158
|
-
def
|
159
|
-
|
160
|
-
|
157
|
+
def match_attributes_and_text(reference, sample)
|
158
|
+
@reference, @sample = reference, sample
|
159
|
+
match_attributes and match_text
|
161
160
|
end
|
162
161
|
|
163
|
-
|
164
|
-
|
165
|
-
|
162
|
+
# TODO document without! and xpath! in the diagnostic
|
163
|
+
# TODO uh, indenting mebbe?
|
164
|
+
|
165
|
+
def match_attributes
|
166
|
+
sort_nodes.each do |attr|
|
167
|
+
case attr.name
|
168
|
+
when 'verbose!' ; verbose_spew(attr)
|
169
|
+
when 'xpath!' ; match_xpath_predicate(attr) or return false
|
170
|
+
else ; match_attribute(attr) or return false
|
171
|
+
end
|
166
172
|
end
|
167
173
|
|
168
|
-
|
169
|
-
# TODO this could use more testage, and it could enforce a link to the parent
|
170
|
-
return samples
|
174
|
+
return true
|
171
175
|
end
|
172
|
-
|
173
|
-
attr_accessor :doc,
|
174
|
-
:scope,
|
175
|
-
:builder
|
176
176
|
|
177
|
-
def
|
178
|
-
@
|
179
|
-
|
177
|
+
def sort_nodes
|
178
|
+
@reference.attribute_nodes.sort_by do |q|
|
179
|
+
{ 'verbose!' => 0, # put this first, so it always runs, even if attributes don't match
|
180
|
+
'xpath!' => 2 # put this last, so if attributes don't match, it does not waste time
|
181
|
+
}.fetch(q.name, 1)
|
182
|
+
end
|
180
183
|
end
|
181
184
|
|
182
|
-
def
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
@
|
189
|
-
next if @path == "//descendant::html[ refer(., '0') ]" # CONSIDER wtf is this?
|
190
|
-
paths << @path
|
185
|
+
def verbose_spew(attr)
|
186
|
+
if attr.value == 'true' and @spewed[yo_path = @sample.path] == nil
|
187
|
+
puts
|
188
|
+
puts '-' * 60
|
189
|
+
p yo_path
|
190
|
+
puts @sample.to_xhtml
|
191
|
+
@spewed[yo_path] = true
|
191
192
|
end
|
193
|
+
end # ERGO this could use a test...
|
192
194
|
|
193
|
-
|
194
|
-
end # TODO refactor more to actually use this
|
195
|
-
|
196
|
-
def matches?(stwing, &block)
|
197
|
-
@scope.wrap_expectation self do
|
198
|
-
begin
|
199
|
-
paths = build_xpaths(&block)
|
200
|
-
@doc = Nokogiri::HTML(stwing)
|
201
|
-
@reason = nil
|
202
|
-
|
203
|
-
@builder.doc.children.each do |child|
|
204
|
-
@first_samples = []
|
205
|
-
@spewed = {}
|
206
|
-
@path = build_deep_xpath(child)
|
207
|
-
next if @path == "//descendant::html[ refer(., '0') ]" # CONSIDER wtf is this?
|
208
|
-
|
209
|
-
matchers = @doc.root.xpath_with_callback @path, :refer do |elements, index|
|
210
|
-
collect_samples(elements, index.to_i)
|
211
|
-
end
|
212
|
-
|
213
|
-
matchers.empty? and assemble_complaint and return false
|
214
|
-
# TODO use or lose @reason
|
215
|
-
end
|
216
|
-
|
217
|
-
# TODO complain if too many matchers
|
195
|
+
# TODO why we have no :css! yet??
|
218
196
|
|
197
|
+
def match_xpath_predicate(attr)
|
198
|
+
@sample.parent.xpath("*[ #{ attr.value } ]").each do |m|
|
199
|
+
m.path == @sample.path and
|
219
200
|
return true
|
220
|
-
end
|
221
201
|
end
|
202
|
+
|
203
|
+
return false
|
222
204
|
end
|
223
205
|
|
224
|
-
def
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
end
|
230
|
-
return '//' + path
|
206
|
+
def match_attribute(attr)
|
207
|
+
ref = deAmpAmp(attr.value)
|
208
|
+
sam = deAmpAmp(@sample[attr.name])
|
209
|
+
ref == sam or match_regexp(ref, sam) or
|
210
|
+
match_class(attr.name, ref, sam)
|
231
211
|
end
|
232
212
|
|
233
|
-
def
|
234
|
-
|
235
|
-
|
213
|
+
def deAmpAmp(stwing)
|
214
|
+
stwing.to_s.gsub('&amp;', '&').gsub('&', '&')
|
215
|
+
end # ERGO await a fix in Nokogiri, and hope nobody actually means &amp; !!!
|
216
|
+
|
217
|
+
def match_regexp(reference, sample)
|
218
|
+
reference =~ /\(\?.*\)/ and # the irony _is_ lost on us...
|
219
|
+
Regexp.new(reference) =~ sample
|
236
220
|
end
|
237
221
|
|
238
|
-
|
222
|
+
def match_class(attr_name, ref, sam)
|
223
|
+
attr_name == 'class' and
|
224
|
+
" #{ sam } ".index(" #{ ref } ")
|
225
|
+
end # NOTE if you call it a class, but ref contains
|
226
|
+
# something fruity, you are on your own!
|
239
227
|
|
240
|
-
def
|
241
|
-
|
242
|
-
|
243
|
-
|
228
|
+
def match_text(ref = @reference, sam = @sample)
|
229
|
+
ref_text = get_texts(ref)
|
230
|
+
ref_text.empty? and return true
|
231
|
+
sam_text = get_texts(sam)
|
232
|
+
(ref_text - sam_text).empty? and return true
|
233
|
+
ref_text.length == 1 and match_regexp(ref_text.first, sam_text.join)
|
234
|
+
end
|
244
235
|
|
245
|
-
|
246
|
-
|
247
|
-
|
236
|
+
def get_texts(element)
|
237
|
+
element.children.grep(Nokogiri::XML::Text).
|
238
|
+
map{|x| x.to_s.strip}.select{|x|x.any?}
|
239
|
+
end
|
240
|
+
|
241
|
+
def collect_best_sample(samples)
|
242
|
+
sample = samples.first or return
|
243
|
+
|
244
|
+
if @best_sample.nil? or depth(@best_sample) > depth(sample)
|
245
|
+
@best_sample = sample
|
248
246
|
end
|
247
|
+
end
|
249
248
|
|
250
|
-
|
249
|
+
def depth(e)
|
250
|
+
e.xpath('ancestor-or-self::*').length
|
251
|
+
end
|
252
|
+
|
253
|
+
def complain( refered = @builder.doc,
|
254
|
+
sample = @best_sample || @doc.root )
|
255
|
+
@failure_message = "\nCould not find this reference...\n\n" +
|
256
|
+
refered.to_html.sub(/^\<\!DOCTYPE.*/, '') +
|
257
|
+
"\n\n...in this sample...\n\n" +
|
258
|
+
sample.to_html
|
251
259
|
end
|
252
260
|
|
253
|
-
def
|
254
|
-
|
255
|
-
@references << element # note we skip the without @reference!
|
256
|
-
|
257
|
-
if element.name == 'without!'
|
258
|
-
return 'not( ' + build_predicate(element, 'or') + '1=1 )'
|
259
|
-
else
|
260
|
-
path = 'descendant::'
|
261
|
-
path << element.name.sub(/\!$/, '')
|
262
|
-
path << '[ '
|
263
|
-
path << build_predicate(element)
|
264
|
-
path << "refer(., '#{count}') ]" # last so boolean short-circuiting optimizes
|
265
|
-
# xpath = element['xpath!']
|
266
|
-
# path << "[ #{ xpath } ]" if xpath
|
267
|
-
return path
|
268
|
-
end
|
261
|
+
def build_deep_xpath_too(element)
|
262
|
+
return '//' + build_xpath_too(element)
|
269
263
|
end
|
270
264
|
|
271
265
|
def build_xpath_too(element)
|
@@ -288,47 +282,14 @@ class BeHtmlWith
|
|
288
282
|
end
|
289
283
|
path << ' and ' if element_kids.any?
|
290
284
|
|
291
|
-
path << "refer(., '#{count}') ]" # last so boolean short-circuiting optimizes
|
285
|
+
path << "refer(., '#{ count }') ]" # last so boolean short-circuiting optimizes
|
292
286
|
return path
|
293
287
|
end
|
294
288
|
|
295
|
-
def complain_about(refered, samples, reason = nil) # TODO put argumnets in order
|
296
|
-
reason = " (#{reason})" if reason
|
297
|
-
"\nCould not find this reference#{reason}...\n\n" +
|
298
|
-
refered.to_html +
|
299
|
-
"\n\n...in these sample(s)...\n\n" + # TODO how many samples?
|
300
|
-
samples.map{|s|s.to_html}.join("\n\n...or...\n\n")
|
301
|
-
end
|
302
|
-
|
303
|
-
def count_elements_to_node(container, element)
|
304
|
-
return 0 if elements_equal(container, element)
|
305
|
-
count = 0
|
306
|
-
|
307
|
-
container.children.each do |child|
|
308
|
-
sub_count = count_elements_to_node(child, element)
|
309
|
-
return count + sub_count if sub_count
|
310
|
-
count += 1
|
311
|
-
end
|
312
|
-
|
313
|
-
return nil
|
314
|
-
end # TODO use or lose these
|
315
|
-
|
316
|
-
attr_accessor :failure_message
|
317
|
-
|
318
289
|
def negative_failure_message
|
319
|
-
"
|
290
|
+
"please don't negate - use without!"
|
320
291
|
end
|
321
292
|
|
322
|
-
def initialize(scope, &block)
|
323
|
-
@scope, @block = scope, block
|
324
|
-
end
|
325
|
-
|
326
|
-
def self.create(stwing)
|
327
|
-
bhw = BeHtmlWith.new(nil)
|
328
|
-
bhw.doc = Nokogiri::HTML(stwing)
|
329
|
-
return bhw
|
330
|
-
end
|
331
|
-
|
332
293
|
end
|
333
294
|
|
334
295
|
|
@@ -336,7 +297,7 @@ module Test; module Unit; module Assertions
|
|
336
297
|
|
337
298
|
def wrap_expectation whatever; yield; end unless defined? wrap_expectation
|
338
299
|
|
339
|
-
def assert_xhtml(xhtml = @response.body, &block) #
|
300
|
+
def assert_xhtml(xhtml = @response.body, &block) # ERGO merge
|
340
301
|
if block
|
341
302
|
matcher = BeHtmlWith.new(self, &block)
|
342
303
|
matcher.matches?(xhtml, &block)
|
@@ -368,4 +329,22 @@ class Nokogiri::XML::Builder
|
|
368
329
|
node = Nokogiri::XML::Text.new(string.to_s, @doc)
|
369
330
|
insert(node)
|
370
331
|
end
|
332
|
+
end # ERGO retire these monkey patches as Nokogiri catches up
|
333
|
+
|
334
|
+
class Nokogiri::XML::Node
|
335
|
+
|
336
|
+
class XPathPredicateYielder
|
337
|
+
def initialize(method_name, &block)
|
338
|
+
self.class.send :define_method, method_name do |*args|
|
339
|
+
raise 'must call with block' unless block
|
340
|
+
block.call(*args)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def xpath_with_callback(path, method_name, &block)
|
346
|
+
xpath path, XPathPredicateYielder.new(method_name, &block)
|
347
|
+
end
|
348
|
+
|
371
349
|
end
|
350
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: assert2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Phlip
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-03-
|
12
|
+
date: 2009-03-28 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|