assert2 0.4.6 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/assert2/xhtml.rb +171 -192
  2. metadata +2 -2
data/lib/assert2/xhtml.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  =begin
2
- One Yury Kotlyarov recently posted this Rails project as a question:
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 Nokogiri::XML::Node
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 xpath_with_callback(path, method_name, &block)
75
- xpath path, XPathYielder.new(method_name, &block)
65
+ def initialize(scope, &block)
66
+ @scope, @block = scope, block
67
+ @references = []
68
+ @spewed = {}
76
69
  end
77
70
 
78
- end
71
+ attr_accessor :builder,
72
+ :doc,
73
+ :failure_message,
74
+ :scope
75
+ attr_reader :references
76
+ attr_writer :reference,
77
+ :sample
79
78
 
80
- class BeHtmlWith
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
- def get_texts(element)
87
- element.xpath('text()').map{|x|x.to_s.strip}.reject{|x|x==''}.compact
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
- def match_regexp(reference, sample)
91
- reference =~ /^\(\?/ and
92
- Regexp.new(reference) =~ sample
92
+ elemental_children.map do |child|
93
+ build_deep_xpath(child)
94
+ end
93
95
  end
94
96
 
95
- def match_text(ref, sam)
96
- ref_text = get_texts(ref)
97
+ def elemental_children(element = @builder.doc)
98
+ element.children.grep(Nokogiri::XML::Element)
99
+ end
97
100
 
98
- ref_text.empty? or ( ref_text - (sam_text = get_texts(sam)) ).empty? or
99
- (ref_text.length == 1 and
100
- match_regexp(ref_text.first, sam_text.join) )
101
- end # The irony _is_ lost on us
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 verbose_spew(reference, sample)
104
- if reference['verbose!'] == 'true' and
105
- @spewed[yo_path = sample.path] == nil
106
- puts
107
- puts '-' * 60
108
- p yo_path
109
- puts sample.to_xhtml
110
- @spewed[yo_path] = true
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 match_class(attr_name, ref, sam)
115
- return false unless attr_name == 'class'
116
- return " #{sam} ".index(" #{ref} ")
117
- end # NOTE if you call it a class, but ref contains
118
- # something fruity, you are on your own!
119
-
120
- def match_attributes(reference, sample)
121
- reference.attribute_nodes.each do |attr|
122
- unless %w( xpath! verbose! ).include? attr.name
123
- ref, sam = deAmpAmp(attr.value),
124
- deAmpAmp(sample[attr.name])
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(reference, sample)
137
- if value = reference['xpath!']
138
- matches = sample.parent.xpath("*[ #{value} ]")
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
- def match_attributes_and_text(reference, sample)
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
- return false
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 elements_equal(element_1, element_2)
159
- raise 'programming error: mismatched elements' unless element_1.document == element_2.document
160
- element_1.path == element_2.path
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
- def collect_samples(elements, index)
164
- samples = elements.find_all do |element|
165
- match_attributes_and_text(@references[index], element)
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
- @first_samples += elements # if index == 0
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 assemble_complaint
178
- @first_samples << @doc.root if @first_samples.empty? # TODO test the first_samples system
179
- @failure_message = complain_about(@builder.doc.root, @first_samples)
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 build_xpaths(&block)
183
- paths = []
184
- bwock = block || @block || proc{} # TODO what to do with no block? validate?
185
- @builder = Nokogiri::HTML::Builder.new(&bwock)
186
-
187
- @builder.doc.children.each do |child|
188
- @path = build_deep_xpath(child)
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
- return paths
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 build_deep_xpath(element)
225
- @references = []
226
- path = build_xpath(element)
227
- if path.index('not') == 0
228
- return '/*[ ' + path + ' ]' # ERGO uh, is there a cleaner way?
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 build_deep_xpath_too(element)
234
- @references = []
235
- return '//' + build_xpath_too(element)
213
+ def deAmpAmp(stwing)
214
+ stwing.to_s.gsub('&amp;amp;', '&').gsub('&amp;', '&')
215
+ end # ERGO await a fix in Nokogiri, and hope nobody actually means &amp;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
- attr_reader :references
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 build_predicate(element, conjunction = 'and')
241
- path = ''
242
- conjunction = " #{ conjunction } "
243
- element_kids = element.children.grep(Nokogiri::XML::Element)
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
- if element_kids.any?
246
- path << element_kids.map{|child| build_xpath(child) }.join(conjunction)
247
- path << ' and '
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
- return path
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 build_xpath(element)
254
- count = @references.length
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
- "TODO"
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) # TODO merge
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.6
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-22 00:00:00 -07:00
12
+ date: 2009-03-28 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15