assert2 0.3.8 → 0.3.9
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/assert2/xhtml.rb +157 -156
- data/lib/assert2/xhtml.rb~ +159 -157
- data/lib/assert2/xpath.rb +1 -2
- data/lib/assert2/xpath.rb~ +213 -0
- metadata +3 -2
data/lib/assert2/xhtml.rb
CHANGED
@@ -56,191 +56,192 @@ requirements:
|
|
56
56
|
- at fault time, the matcher prints out the failing elements
|
57
57
|
and their immediate context.
|
58
58
|
|
59
|
-
First, we take care of the paperwork. This spec works with Yuri's
|
60
|
-
sample website. I add Nokogiri, for our XML engine:
|
61
59
|
=end
|
62
60
|
|
63
61
|
require 'nokogiri'
|
64
62
|
|
65
|
-
|
66
|
-
That block after "response.body.should be_html_with" answers
|
67
|
-
Yuri's question. Any HTML we can think of, we can specify
|
68
|
-
it in there.
|
63
|
+
class Nokogiri::XML::Node
|
69
64
|
|
70
|
-
|
71
|
-
|
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
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
<legend>Personal Information</legend>
|
77
|
-
<ol>
|
78
|
-
<li id="control_user_first_name">
|
79
|
-
<label for="user_first_name">First name</label>
|
80
|
-
<input type="text" name="user[first_name]" id="user_first_name">
|
81
|
-
</li>
|
82
|
-
</ol>
|
83
|
-
</fieldset>
|
74
|
+
def xpath_with_callback(path, method_name, &block)
|
75
|
+
xpath path, XPathYielder.new(method_name, &block)
|
76
|
+
end
|
84
77
|
|
85
|
-
|
86
|
-
context - the <fieldset> where the matcher sought the
|
87
|
-
errant <input> field. It would not, for example, spew
|
88
|
-
an entire website into our faces.
|
78
|
+
end
|
89
79
|
|
90
|
-
|
91
|
-
RSpec "matcher":
|
92
|
-
=end
|
80
|
+
class BeHtmlWith
|
93
81
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# end
|
82
|
+
def get_texts(element)
|
83
|
+
element.xpath('text()').map{|x|x.to_s.strip}.reject{|x|x==''}.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
def match_text(ref, sam)
|
87
|
+
ref_text = get_texts(ref)
|
88
|
+
# TODO regices?
|
89
|
+
ref_text.empty? or ( ref_text - get_texts(sam) ).empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def match_attributes_and_text(reference, sample)
|
93
|
+
reference.attribute_nodes.each do |attr|
|
94
|
+
sample[attr.name] == attr.value or return false
|
108
95
|
end
|
109
96
|
|
110
|
-
|
111
|
-
|
112
|
-
the matching block into it - that's where all the 'form',
|
113
|
-
'fieldset', 'input', etc. elements came from. And this trick
|
114
|
-
exposes both our target page and our matched elements to the
|
115
|
-
full power of Nokogiri. Schema validation, for example, would
|
116
|
-
be very easy.
|
117
|
-
|
118
|
-
The matches? method works by building two DOMs, and forcing
|
119
|
-
our page's DOM to satisfy each element, attribute, and text
|
120
|
-
in our specification's DOM.
|
121
|
-
|
122
|
-
To match nodes, we first find all nodes, by name, below
|
123
|
-
the current node. Note that match_nodes() recurses. Then
|
124
|
-
we throw away all nodes that don't satisfy our matching
|
125
|
-
criteria.
|
126
|
-
|
127
|
-
We pick the first node that passes that check, and
|
128
|
-
then recursively match its children to each child,
|
129
|
-
if any, from our matching node.
|
130
|
-
=end
|
131
|
-
|
132
|
-
# TODO does a multi-modal top axis work?
|
133
|
-
|
134
|
-
def match_nodes(match, doc)
|
135
|
-
node = doc.xpath("descendant::#{match.name.sub(/\!$/, '')}").
|
136
|
-
select{|n| resemble(match, n) }.
|
137
|
-
first or return complaint(match, doc)
|
138
|
-
|
139
|
-
this_match = node.xpath('preceding::*').length
|
140
|
-
|
141
|
-
if @last_match > this_match
|
142
|
-
return complaint(match, doc, 'node is out of specified order!')
|
143
|
-
end
|
144
|
-
|
145
|
-
@last_match = this_match
|
146
|
-
|
147
|
-
# http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
|
148
|
-
# The preceding axis contains all nodes in the same document
|
149
|
-
# as the context node that are before the context node in
|
150
|
-
# document order, excluding any ancestors and excluding
|
151
|
-
# attribute nodes and namespace nodes
|
152
|
-
|
153
|
-
#p [node.name, node.text]
|
154
|
-
# p node.path if lastest
|
155
|
-
#p node.text
|
156
|
-
# p lastest.path if lastest
|
157
|
-
|
158
|
-
# TODO try xpath('*')
|
159
|
-
match.children.grep(Nokogiri::XML::Element).each do |child|
|
160
|
-
issue = match_nodes(child, node) and
|
161
|
-
return issue
|
162
|
-
end
|
97
|
+
return match_text(reference, sample)
|
98
|
+
end
|
163
99
|
|
164
|
-
|
100
|
+
def elements_equal(element_1, element_2)
|
101
|
+
raise 'programming error: mismatched elements' unless element_1.document == element_2.document
|
102
|
+
element_1.path == element_2.path
|
103
|
+
end
|
104
|
+
|
105
|
+
# end # TODO more "elements" less "nodes"
|
106
|
+
|
107
|
+
def collect_samples(elements, index)
|
108
|
+
samples = elements.find_all do |element|
|
109
|
+
match_attributes_and_text(@references[index], element)
|
165
110
|
end
|
166
|
-
|
167
|
-
=begin
|
168
|
-
At any point in that recursion, if we can't find a match,
|
169
|
-
we build a string describing that situation, and pass it
|
170
|
-
back up the call stack. This immediately stops any iterating
|
171
|
-
and recursing underway!
|
172
|
-
|
173
|
-
Two nodes "resemble" each other if their names are the
|
174
|
-
same (naturally!); if your matching element's
|
175
|
-
attributes are a subset of your page's element's
|
176
|
-
attributes, and if their text is similar:
|
177
|
-
=end
|
178
111
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
112
|
+
@first_samples += samples if samples.any? and index == 0
|
113
|
+
return samples
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_accessor :doc,
|
117
|
+
:scope
|
118
|
+
|
119
|
+
def matches?(stwing, &block)
|
120
|
+
@scope.wrap_expectation self do # TODO put that back online
|
121
|
+
begin
|
122
|
+
bwock = block || @block || proc{} # TODO what to do with no block? validate?
|
123
|
+
builder = Nokogiri::HTML::Builder.new(&bwock)
|
124
|
+
@doc = Nokogiri::HTML(stwing)
|
125
|
+
@reason = nil
|
126
|
+
|
127
|
+
builder.doc.children.each do |child|
|
128
|
+
@first_samples = []
|
129
|
+
# TODO warn if child is text
|
130
|
+
path = build_deep_xpath(child)
|
131
|
+
next if path == "//html[ refer(., '0') ]" # CONSIDER wtf is this?
|
132
|
+
|
133
|
+
matchers = doc.root.xpath_with_callback path, :refer do |elements, index|
|
134
|
+
collect_samples(elements, index.to_i)
|
135
|
+
end
|
136
|
+
|
137
|
+
if matchers.empty?
|
138
|
+
@first_samples << @doc.root if @first_samples.empty? # TODO test the first_samples system
|
139
|
+
@failure_message = complain_about(builder.doc.root, @first_samples)
|
140
|
+
return false
|
141
|
+
end # TODO use or lose @reason
|
142
|
+
end
|
184
143
|
|
185
|
-
|
186
|
-
# match_text = match.xpath('text()').map{|x|x.to_s}
|
187
|
-
# node_text = match.xpath('text()').map{|x|x.to_s}
|
144
|
+
# TODO complain if too many matchers
|
188
145
|
|
189
|
-
|
190
|
-
|
191
|
-
match_text.empty? or 0 == ( match_text - node_text ).length
|
146
|
+
return true
|
147
|
+
end
|
192
148
|
end
|
149
|
+
end
|
193
150
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
Put another way, <form> does not appear to contain "First name".
|
202
|
-
Specifications can only match text by declaring their immediate
|
203
|
-
parent.
|
204
|
-
|
205
|
-
The remaining support methods are self-explanatory. They
|
206
|
-
prepare Node attributes for comparison, build our diagnostics,
|
207
|
-
and plug our matcher object into RSpec:
|
208
|
-
=end
|
209
|
-
|
210
|
-
def valuate(attributes)
|
211
|
-
attributes.inject({}) do |h,(k,v)|
|
212
|
-
h.merge(k => v.value)
|
213
|
-
end # this converts objects to strings, so our Hashes
|
214
|
-
end # can compare for equality
|
215
|
-
|
216
|
-
def complaint(node, match, berate = nil)
|
217
|
-
"\n #{berate}".rstrip +
|
218
|
-
"\n\n#{node.to_html}\n" +
|
219
|
-
" does not match\n\n" +
|
220
|
-
match.to_html
|
221
|
-
end
|
151
|
+
def build_deep_xpath(element)
|
152
|
+
@references = []
|
153
|
+
return '//' + build_xpath(element)
|
154
|
+
end
|
155
|
+
|
156
|
+
attr_reader :references
|
222
157
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
158
|
+
def build_xpath(element)
|
159
|
+
path = element.name.sub(/\!$/, '')
|
160
|
+
element_kids = element.children.grep(Nokogiri::XML::Element)
|
161
|
+
path << "[ refer(., '#{@references.length}')"
|
162
|
+
@references << element
|
163
|
+
|
164
|
+
if element_kids.any?
|
165
|
+
path << ' and ' +
|
166
|
+
element_kids.map{|child|
|
167
|
+
'./descendant::' + build_xpath(child)
|
168
|
+
}.join(' and ')
|
227
169
|
end
|
170
|
+
|
171
|
+
path << ' ]'
|
172
|
+
return path
|
173
|
+
end
|
174
|
+
|
175
|
+
def complain_about(refered, samples, reason = nil) # TODO put argumnets in order
|
176
|
+
reason = " (#{reason})" if reason
|
177
|
+
"\nCould not find this reference#{reason}...\n\n" +
|
178
|
+
refered.to_html +
|
179
|
+
"\n\n...in these sample(s)...\n\n" + # TODO how many samples?
|
180
|
+
samples.map{|s|s.to_html}.join("\n\n...or...\n\n")
|
181
|
+
end
|
182
|
+
|
183
|
+
def count_elements_to_node(container, element)
|
184
|
+
return 0 if elements_equal(container, element)
|
185
|
+
count = 0
|
228
186
|
|
229
|
-
|
230
|
-
|
187
|
+
container.children.each do |child|
|
188
|
+
sub_count = count_elements_to_node(child, element)
|
189
|
+
return count + sub_count if sub_count
|
190
|
+
count += 1
|
231
191
|
end
|
192
|
+
|
193
|
+
return nil
|
194
|
+
end # TODO use or lose these
|
195
|
+
|
196
|
+
# TODO does a multi-modal top axis work?
|
197
|
+
# TODO this_match = node.xpath('preceding::*').length
|
198
|
+
|
199
|
+
# http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
|
200
|
+
# The preceding axis contains all nodes in the same document
|
201
|
+
# as the context node that are before the context node in
|
202
|
+
# document order, excluding any ancestors and excluding
|
203
|
+
# attribute nodes and namespace nodes
|
204
|
+
|
205
|
+
attr_accessor :failure_message
|
206
|
+
|
207
|
+
def negative_failure_message
|
208
|
+
"TODO"
|
209
|
+
end
|
210
|
+
|
211
|
+
def initialize(scope, &block)
|
212
|
+
@scope, @block = scope, block
|
213
|
+
end
|
214
|
+
|
215
|
+
def self.create(stwing)
|
216
|
+
bhw = BeHtmlWith.new(nil)
|
217
|
+
bhw.doc = Nokogiri::HTML(stwing)
|
218
|
+
return bhw
|
232
219
|
end
|
233
220
|
|
234
|
-
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
module Test; module Unit; module Assertions
|
225
|
+
|
226
|
+
def wrap_expectation whatever; yield; end unless defined? wrap_expectation
|
227
|
+
|
235
228
|
def assert_xhtml(xhtml = @response.body, &block) # TODO merge
|
236
|
-
_assert_xml(xhtml) # , XML::HTMLParser)
|
237
229
|
if block
|
238
|
-
# require 'should_be_html_with_spec'
|
239
230
|
matcher = BeHtmlWith.new(self, &block)
|
240
231
|
matcher.matches?(xhtml, &block)
|
241
232
|
message = matcher.failure_message
|
242
233
|
flunk message if message.to_s != ''
|
234
|
+
# return matcher.builder.doc.to_html # TODO return something reasonable
|
235
|
+
else
|
236
|
+
_assert_xml(xhtml)
|
237
|
+
return @xdoc
|
243
238
|
end
|
244
|
-
return @xdoc
|
245
239
|
end
|
246
|
-
|
240
|
+
|
241
|
+
end; end; end
|
242
|
+
|
243
|
+
module Spec; module Matchers
|
244
|
+
def be_html_with(&block)
|
245
|
+
BeHtmlWith.new(self, &block)
|
246
|
+
end
|
247
|
+
end; end
|
data/lib/assert2/xhtml.rb~
CHANGED
@@ -56,191 +56,193 @@ requirements:
|
|
56
56
|
- at fault time, the matcher prints out the failing elements
|
57
57
|
and their immediate context.
|
58
58
|
|
59
|
-
First, we take care of the paperwork. This spec works with Yuri's
|
60
|
-
sample website. I add Nokogiri, for our XML engine:
|
61
59
|
=end
|
62
60
|
|
63
61
|
require 'nokogiri'
|
64
62
|
|
65
|
-
|
66
|
-
That block after "response.body.should be_html_with" answers
|
67
|
-
Yuri's question. Any HTML we can think of, we can specify
|
68
|
-
it in there.
|
63
|
+
class Nokogiri::XML::Node
|
69
64
|
|
70
|
-
|
71
|
-
|
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
|
72
73
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
<legend>Personal Information</legend>
|
77
|
-
<ol>
|
78
|
-
<li id="control_user_first_name">
|
79
|
-
<label for="user_first_name">First name</label>
|
80
|
-
<input type="text" name="user[first_name]" id="user_first_name">
|
81
|
-
</li>
|
82
|
-
</ol>
|
83
|
-
</fieldset>
|
74
|
+
def xpath_with_callback(path, method_name, &block)
|
75
|
+
xpath path, XPathYielder.new(method_name, &block)
|
76
|
+
end
|
84
77
|
|
85
|
-
|
86
|
-
context - the <fieldset> where the matcher sought the
|
87
|
-
errant <input> field. It would not, for example, spew
|
88
|
-
an entire website into our faces.
|
78
|
+
end
|
89
79
|
|
90
|
-
|
91
|
-
RSpec "matcher":
|
92
|
-
=end
|
80
|
+
class BeHtmlWith
|
93
81
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
# end
|
82
|
+
def get_texts(element)
|
83
|
+
element.xpath('text()').map{|x|x.to_s.strip}.reject{|x|x==''}.compact
|
84
|
+
end
|
85
|
+
|
86
|
+
def match_text(ref, sam)
|
87
|
+
ref_text = get_texts(ref)
|
88
|
+
# TODO regices?
|
89
|
+
ref_text.empty? or ( ref_text - get_texts(sam) ).empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def match_attributes_and_text(reference, sample)
|
93
|
+
reference.attribute_nodes.each do |attr|
|
94
|
+
sample[attr.name] == attr.value or return false
|
108
95
|
end
|
109
96
|
|
110
|
-
|
111
|
-
|
112
|
-
the matching block into it - that's where all the 'form',
|
113
|
-
'fieldset', 'input', etc. elements came from. And this trick
|
114
|
-
exposes both our target page and our matched elements to the
|
115
|
-
full power of Nokogiri. Schema validation, for example, would
|
116
|
-
be very easy.
|
117
|
-
|
118
|
-
The matches? method works by building two DOMs, and forcing
|
119
|
-
our page's DOM to satisfy each element, attribute, and text
|
120
|
-
in our specification's DOM.
|
121
|
-
|
122
|
-
To match nodes, we first find all nodes, by name, below
|
123
|
-
the current node. Note that match_nodes() recurses. Then
|
124
|
-
we throw away all nodes that don't satisfy our matching
|
125
|
-
criteria.
|
126
|
-
|
127
|
-
We pick the first node that passes that check, and
|
128
|
-
then recursively match its children to each child,
|
129
|
-
if any, from our matching node.
|
130
|
-
=end
|
131
|
-
|
132
|
-
# TODO does a multi-modal top axis work?
|
133
|
-
|
134
|
-
def match_nodes(match, doc)
|
135
|
-
node = doc.xpath("descendant::#{match.name.sub(/\!$/, '')}").
|
136
|
-
select{|n| resemble(match, n) }.
|
137
|
-
first or return complaint(match, doc)
|
138
|
-
|
139
|
-
this_match = node.xpath('preceding::*').length
|
140
|
-
|
141
|
-
if @last_match > this_match
|
142
|
-
return complaint(match, doc, 'node is out of specified order!')
|
143
|
-
end
|
144
|
-
|
145
|
-
@last_match = this_match
|
146
|
-
|
147
|
-
# http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
|
148
|
-
# The preceding axis contains all nodes in the same document
|
149
|
-
# as the context node that are before the context node in
|
150
|
-
# document order, excluding any ancestors and excluding
|
151
|
-
# attribute nodes and namespace nodes
|
152
|
-
|
153
|
-
#p [node.name, node.text]
|
154
|
-
# p node.path if lastest
|
155
|
-
#p node.text
|
156
|
-
# p lastest.path if lastest
|
157
|
-
|
158
|
-
# TODO try xpath('*')
|
159
|
-
match.children.grep(Nokogiri::XML::Element).each do |child|
|
160
|
-
issue = match_nodes(child, node) and
|
161
|
-
return issue
|
162
|
-
end
|
97
|
+
return match_text(reference, sample)
|
98
|
+
end
|
163
99
|
|
164
|
-
|
100
|
+
def elements_equal(element_1, element_2)
|
101
|
+
raise 'programming error: mismatched elements' unless element_1.document == element_2.document
|
102
|
+
element_1.path == element_2.path
|
103
|
+
end
|
104
|
+
|
105
|
+
# end # TODO more "elements" less "nodes"
|
106
|
+
|
107
|
+
def collect_samples(elements, index)
|
108
|
+
samples = elements.find_all do |element|
|
109
|
+
match_attributes_and_text(@references[index], element)
|
165
110
|
end
|
166
|
-
|
167
|
-
=begin
|
168
|
-
At any point in that recursion, if we can't find a match,
|
169
|
-
we build a string describing that situation, and pass it
|
170
|
-
back up the call stack. This immediately stops any iterating
|
171
|
-
and recursing underway!
|
172
|
-
|
173
|
-
Two nodes "resemble" each other if their names are the
|
174
|
-
same (naturally!); if your matching element's
|
175
|
-
attributes are a subset of your page's element's
|
176
|
-
attributes, and if their text is similar:
|
177
|
-
=end
|
178
111
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
112
|
+
@first_samples += samples if samples.any? and index == 0
|
113
|
+
return samples
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_accessor :doc,
|
117
|
+
:scope
|
118
|
+
|
119
|
+
def matches?(stwing, &block)
|
120
|
+
@scope.wrap_expectation self do # TODO put that back online
|
121
|
+
begin
|
122
|
+
bwock = block || @block || proc{} # TODO what to do with no block? validate?
|
123
|
+
builder = Nokogiri::HTML::Builder.new(&bwock)
|
124
|
+
puts builder.doc.root.to_html
|
125
|
+
@doc = Nokogiri::HTML(stwing)
|
126
|
+
@reason = nil
|
127
|
+
|
128
|
+
builder.doc.children.each do |child|
|
129
|
+
@first_samples = []
|
130
|
+
# TODO warn if child is text
|
131
|
+
path = build_deep_xpath(child)
|
132
|
+
next if path == "//html[ refer(., '0') ]" # CONSIDER wtf is this?
|
133
|
+
|
134
|
+
matchers = doc.root.xpath_with_callback path, :refer do |elements, index|
|
135
|
+
collect_samples(elements, index.to_i)
|
136
|
+
end
|
137
|
+
|
138
|
+
if matchers.empty?
|
139
|
+
@first_samples << @doc.root if @first_samples.empty? # TODO test the first_samples system
|
140
|
+
@failure_message = complain_about(builder.doc.root, @first_samples)
|
141
|
+
return false
|
142
|
+
end # TODO use or lose @reason
|
143
|
+
end
|
184
144
|
|
185
|
-
|
186
|
-
# match_text = match.xpath('text()').map{|x|x.to_s}
|
187
|
-
# node_text = match.xpath('text()').map{|x|x.to_s}
|
145
|
+
# TODO complain if too many matchers
|
188
146
|
|
189
|
-
|
190
|
-
|
191
|
-
match_text.empty? or 0 == ( match_text - node_text ).length
|
147
|
+
return true
|
148
|
+
end
|
192
149
|
end
|
150
|
+
end
|
193
151
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
Put another way, <form> does not appear to contain "First name".
|
202
|
-
Specifications can only match text by declaring their immediate
|
203
|
-
parent.
|
204
|
-
|
205
|
-
The remaining support methods are self-explanatory. They
|
206
|
-
prepare Node attributes for comparison, build our diagnostics,
|
207
|
-
and plug our matcher object into RSpec:
|
208
|
-
=end
|
209
|
-
|
210
|
-
def valuate(attributes)
|
211
|
-
attributes.inject({}) do |h,(k,v)|
|
212
|
-
h.merge(k => v.value)
|
213
|
-
end # this converts objects to strings, so our Hashes
|
214
|
-
end # can compare for equality
|
215
|
-
|
216
|
-
def complaint(node, match, berate = nil)
|
217
|
-
"\n #{berate}".rstrip +
|
218
|
-
"\n\n#{node.to_html}\n" +
|
219
|
-
" does not match\n\n" +
|
220
|
-
match.to_html
|
221
|
-
end
|
152
|
+
def build_deep_xpath(element)
|
153
|
+
@references = []
|
154
|
+
return '//' + build_xpath(element)
|
155
|
+
end
|
156
|
+
|
157
|
+
attr_reader :references
|
222
158
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
159
|
+
def build_xpath(element)
|
160
|
+
path = element.name.sub(/\!$/, '')
|
161
|
+
element_kids = element.children.grep(Nokogiri::XML::Element)
|
162
|
+
path << "[ refer(., '#{@references.length}')"
|
163
|
+
@references << element
|
164
|
+
|
165
|
+
if element_kids.any?
|
166
|
+
path << ' and ' +
|
167
|
+
element_kids.map{|child|
|
168
|
+
'./descendant::' + build_xpath(child)
|
169
|
+
}.join(' and ')
|
227
170
|
end
|
171
|
+
|
172
|
+
path << ' ]'
|
173
|
+
return path
|
174
|
+
end
|
175
|
+
|
176
|
+
def complain_about(refered, samples, reason = nil) # TODO put argumnets in order
|
177
|
+
reason = " (#{reason})" if reason
|
178
|
+
"\nCould not find this reference#{reason}...\n\n" +
|
179
|
+
refered.to_html +
|
180
|
+
"\n\n...in these sample(s)...\n\n" + # TODO how many samples?
|
181
|
+
samples.map{|s|s.to_html}.join("\n\n...or...\n\n")
|
182
|
+
end
|
183
|
+
|
184
|
+
def count_elements_to_node(container, element)
|
185
|
+
return 0 if elements_equal(container, element)
|
186
|
+
count = 0
|
228
187
|
|
229
|
-
|
230
|
-
|
188
|
+
container.children.each do |child|
|
189
|
+
sub_count = count_elements_to_node(child, element)
|
190
|
+
return count + sub_count if sub_count
|
191
|
+
count += 1
|
231
192
|
end
|
193
|
+
|
194
|
+
return nil
|
195
|
+
end # TODO use or lose these
|
196
|
+
|
197
|
+
# TODO does a multi-modal top axis work?
|
198
|
+
# TODO this_match = node.xpath('preceding::*').length
|
199
|
+
|
200
|
+
# http://www.zvon.org/xxl/XPathTutorial/Output/example18.html
|
201
|
+
# The preceding axis contains all nodes in the same document
|
202
|
+
# as the context node that are before the context node in
|
203
|
+
# document order, excluding any ancestors and excluding
|
204
|
+
# attribute nodes and namespace nodes
|
205
|
+
|
206
|
+
attr_accessor :failure_message
|
207
|
+
|
208
|
+
def negative_failure_message
|
209
|
+
"TODO"
|
210
|
+
end
|
211
|
+
|
212
|
+
def initialize(scope, &block)
|
213
|
+
@scope, @block = scope, block
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.create(stwing)
|
217
|
+
bhw = BeHtmlWith.new(nil)
|
218
|
+
bhw.doc = Nokogiri::HTML(stwing)
|
219
|
+
return bhw
|
232
220
|
end
|
233
221
|
|
234
|
-
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
module Test; module Unit; module Assertions
|
226
|
+
|
227
|
+
def wrap_expectation whatever; yield; end unless defined? wrap_expectation
|
228
|
+
|
235
229
|
def assert_xhtml(xhtml = @response.body, &block) # TODO merge
|
236
|
-
_assert_xml(xhtml) # , XML::HTMLParser)
|
237
230
|
if block
|
238
|
-
# require 'should_be_html_with_spec'
|
239
231
|
matcher = BeHtmlWith.new(self, &block)
|
240
232
|
matcher.matches?(xhtml, &block)
|
241
233
|
message = matcher.failure_message
|
242
|
-
flunk message if message.
|
234
|
+
flunk message if message.to_s != ''
|
235
|
+
# return matcher.builder.doc.to_html # TODO return something reasonable
|
236
|
+
else
|
237
|
+
_assert_xml(xhtml)
|
238
|
+
return @xdoc
|
243
239
|
end
|
244
|
-
return @xdoc
|
245
240
|
end
|
246
|
-
|
241
|
+
|
242
|
+
end; end; end
|
243
|
+
|
244
|
+
module Spec; module Matchers
|
245
|
+
def be_html_with(&block)
|
246
|
+
BeHtmlWith.new(self, &block)
|
247
|
+
end
|
248
|
+
end; end
|
data/lib/assert2/xpath.rb
CHANGED
@@ -131,8 +131,7 @@ module Test; module Unit; module Assertions
|
|
131
131
|
former_xdoc = @xdoc
|
132
132
|
apa = AssertXPathArguments.new(path, id, options)
|
133
133
|
node = @xdoc.xpath(apa.xpath) #, nil, apa.subs)
|
134
|
-
|
135
|
-
|
134
|
+
|
136
135
|
add_diagnostic :clear do
|
137
136
|
diagnostic = "xpath: #{ apa.xpath.inspect }\n"
|
138
137
|
diagnostic << "arguments: #{ apa.subs.pretty_inspect }\n" if apa.subs.any?
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'assert2'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'rexml/entity'
|
5
|
+
require 'rexml/formatters/pretty'
|
6
|
+
require 'nokogiri' # must be installed to use xpath{}!
|
7
|
+
|
8
|
+
module Test; module Unit; module Assertions
|
9
|
+
|
10
|
+
def assert_xhtml(xhtml)
|
11
|
+
return _assert_xml(xhtml) # , XML::HTMLParser)
|
12
|
+
end
|
13
|
+
|
14
|
+
def _assert_xml(xml) #, parser = XML::Parser)
|
15
|
+
if false
|
16
|
+
xp = parser.new()
|
17
|
+
xp.string = xml
|
18
|
+
|
19
|
+
if XML.respond_to? :'default_pedantic_parser='
|
20
|
+
XML.default_pedantic_parser = true
|
21
|
+
else
|
22
|
+
XML::Parser.default_pedantic_parser = true
|
23
|
+
end # CONSIDER uh, figure out the best libxml-ruby??
|
24
|
+
|
25
|
+
@xdoc = xp.parse.root
|
26
|
+
else
|
27
|
+
# CONSIDER figure out how entities are supposed to work!!
|
28
|
+
xml = xml.gsub('—', '--')
|
29
|
+
doc = REXML::Document.new(xml)
|
30
|
+
@xdoc = doc.root
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_xhtml_(xhtml)
|
35
|
+
return _assert_xml_(xhtml) # , XML::HTMLParser)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _assert_xml_(xml) #, parser = XML::Parser)
|
39
|
+
if false
|
40
|
+
xp = parser.new()
|
41
|
+
xp.string = xml
|
42
|
+
|
43
|
+
if XML.respond_to? :'default_pedantic_parser='
|
44
|
+
XML.default_pedantic_parser = true
|
45
|
+
else
|
46
|
+
XML::Parser.default_pedantic_parser = true
|
47
|
+
end # CONSIDER uh, figure out the best libxml-ruby??
|
48
|
+
|
49
|
+
@xdoc = xp.parse.root
|
50
|
+
else
|
51
|
+
# TODO figure out how entities are supposed to work!!
|
52
|
+
xml = xml.gsub('—', '--')
|
53
|
+
doc = Nokogiri::XML(xml)
|
54
|
+
@xdoc = doc.root
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class AssertXPathArguments
|
59
|
+
|
60
|
+
def initialize(path = '', id = nil, options = {})
|
61
|
+
@subs = {}
|
62
|
+
@xpath = ''
|
63
|
+
to_xpath(path, id, options)
|
64
|
+
end
|
65
|
+
|
66
|
+
attr_reader :subs
|
67
|
+
attr_reader :xpath
|
68
|
+
|
69
|
+
def to_conditions(hash)
|
70
|
+
xml_attribute_name = /^[a-z][_a-z0-9]+$/i # CONSIDER is that an XML attribute name match?
|
71
|
+
|
72
|
+
@xpath << hash.map{|k, v|
|
73
|
+
sk = k.to_s
|
74
|
+
sk = '_text' if sk == '.' or k == 46
|
75
|
+
k = '.' if k == 46 and RUBY_VERSION < '1.9.0'
|
76
|
+
@subs[sk] = v.to_s
|
77
|
+
"#{ '@' if k.to_s =~ xml_attribute_name }#{k} = $#{sk}"
|
78
|
+
}.join(' and ')
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_predicate(hash, options)
|
82
|
+
hash = { :id => hash } if hash.kind_of? Symbol
|
83
|
+
hash.merge! options
|
84
|
+
@xpath << '[ '
|
85
|
+
to_conditions(hash)
|
86
|
+
@xpath << ' ]'
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_xpath(path, id, options)
|
90
|
+
@xpath = path
|
91
|
+
@xpath = "descendant-or-self::#{ @xpath }" if @xpath.kind_of? Symbol
|
92
|
+
to_predicate(id, options) if id
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# if node = @xdoc.find_first(path) ## for libxml
|
98
|
+
# def node.text
|
99
|
+
# find_first('text()').to_s
|
100
|
+
# end
|
101
|
+
|
102
|
+
def xpath(path, id = nil, options = {}, &block)
|
103
|
+
former_xdoc = @xdoc
|
104
|
+
apa = AssertXPathArguments.new(path, id, options)
|
105
|
+
node = REXML::XPath.first(@xdoc, apa.xpath, nil, apa.subs)
|
106
|
+
|
107
|
+
add_diagnostic :clear do
|
108
|
+
diagnostic = "xpath: #{ apa.xpath.inspect }\n"
|
109
|
+
diagnostic << "arguments: #{ apa.subs.pretty_inspect }\n" if apa.subs.any?
|
110
|
+
diagnostic + "xml context:\n" + indent_xml
|
111
|
+
end
|
112
|
+
|
113
|
+
if node
|
114
|
+
def node.[](symbol)
|
115
|
+
return attributes[symbol.to_s]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if block
|
120
|
+
assert_('this xpath cannot find a node', :keep_diagnostics => true){ node }
|
121
|
+
assert_ nil, :args => [@xdoc = node], :keep_diagnostics => true, &block # TODO need the _ ?
|
122
|
+
end
|
123
|
+
|
124
|
+
return node
|
125
|
+
# TODO raid http://thebogles.com/blog/an-hpricot-style-interface-to-libxml/
|
126
|
+
ensure
|
127
|
+
@xdoc = former_xdoc
|
128
|
+
end # TODO trap LibXML::XML::XPath::InvalidPath and explicate it's an XPath problem
|
129
|
+
|
130
|
+
def xpath_(path, id = nil, options = {}, &block)
|
131
|
+
former_xdoc = @xdoc
|
132
|
+
apa = AssertXPathArguments.new(path, id, options)
|
133
|
+
node = @xdoc.xpath(apa.xpath) #, nil, apa.subs)
|
134
|
+
# TODO advise Nokogiri to provide substitution arguments
|
135
|
+
|
136
|
+
add_diagnostic :clear do
|
137
|
+
diagnostic = "xpath: #{ apa.xpath.inspect }\n"
|
138
|
+
diagnostic << "arguments: #{ apa.subs.pretty_inspect }\n" if apa.subs.any?
|
139
|
+
diagnostic + "xml context:\n" + indent_xml
|
140
|
+
end
|
141
|
+
|
142
|
+
if node
|
143
|
+
def node.[](symbol)
|
144
|
+
return attributes[symbol.to_s]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
if block
|
149
|
+
assert_('this xpath cannot find a node', :keep_diagnostics => true){ node }
|
150
|
+
assert_ nil, :args => [@xdoc = node], :keep_diagnostics => true, &block # TODO need the _ ?
|
151
|
+
end
|
152
|
+
|
153
|
+
return node
|
154
|
+
# TODO raid http://thebogles.com/blog/an-hpricot-style-interface-to-libxml/
|
155
|
+
ensure
|
156
|
+
@xdoc = former_xdoc
|
157
|
+
end # TODO trap LibXML::XML::XPath::InvalidPath and explicate it's an XPath problem
|
158
|
+
|
159
|
+
def indent_xml(node = @xdoc)
|
160
|
+
bar = REXML::Formatters::Pretty.new
|
161
|
+
out = String.new
|
162
|
+
bar.write(node, out)
|
163
|
+
return out
|
164
|
+
end
|
165
|
+
|
166
|
+
end; end; end
|
167
|
+
|
168
|
+
|
169
|
+
if RUBY_VERSION <= '1.8.6'
|
170
|
+
|
171
|
+
module REXML
|
172
|
+
module Formatters
|
173
|
+
class Pretty
|
174
|
+
|
175
|
+
private
|
176
|
+
|
177
|
+
# see http://www.google.com/codesearch/p?hl=en#Ezb_-tQR858/test_libs/rexml_fix.rb
|
178
|
+
# for less info about this fix...
|
179
|
+
|
180
|
+
def wrap(string, width)
|
181
|
+
# Recursivly wrap string at width.
|
182
|
+
return string if string.length <= width
|
183
|
+
place = string.rindex(/\s+/, width) # Position in string with last ' ' before cutoff
|
184
|
+
return string if place.nil?
|
185
|
+
return string[0,place] + "\n" + wrap(string[place+1..-1], width)
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Element
|
192
|
+
# this patches http://www.germane-software.com/projects/rexml/ticket/128
|
193
|
+
def write(output=$stdout, indent=-1, transitive=false, ie_hack=false)
|
194
|
+
Kernel.warn("#{self.class.name}.write is deprecated. See REXML::Formatters")
|
195
|
+
formatter = if indent > -1
|
196
|
+
if transitive
|
197
|
+
require "rexml/formatters/transitive"
|
198
|
+
REXML::Formatters::Transitive.new( indent, ie_hack )
|
199
|
+
else
|
200
|
+
REXML::Formatters::Pretty.new( indent, ie_hack )
|
201
|
+
end
|
202
|
+
else
|
203
|
+
REXML::Formatters::Default.new( ie_hack )
|
204
|
+
end
|
205
|
+
formatter.write( self, output )
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
require '../../test/assert_xhtml_suite.rb' if $0 == __FILE__ and File.exist?('../../test/assert_xhtml_suite.rb')
|
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.3.
|
4
|
+
version: 0.3.9
|
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-15 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -25,6 +25,7 @@ files:
|
|
25
25
|
- lib/assert2
|
26
26
|
- lib/assert2/xhtml.rb~
|
27
27
|
- lib/assert2/flunk.rb
|
28
|
+
- lib/assert2/xpath.rb~
|
28
29
|
- lib/assert2/rubynode_reflector.rb
|
29
30
|
- lib/assert2/xpath.rb
|
30
31
|
- lib/assert2/ripper_reflector.rb
|