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