assert2 0.3.8 → 0.3.9
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 +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
|