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.
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