xpath 2.0.0 → 3.2.0
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.
- checksums.yaml +7 -0
- data/README.md +6 -64
- data/lib/xpath/dsl.rb +141 -79
- data/lib/xpath/expression.rb +4 -2
- data/lib/xpath/literal.rb +2 -0
- data/lib/xpath/renderer.rb +34 -82
- data/lib/xpath/union.rb +4 -2
- data/lib/xpath/version.rb +3 -1
- data/lib/xpath.rb +4 -4
- data/spec/fixtures/simple.html +11 -3
- data/spec/spec_helper.rb +7 -0
- data/spec/union_spec.rb +15 -16
- data/spec/xpath_spec.rb +336 -123
- metadata +50 -80
- data/lib/xpath/html.rb +0 -175
- data/spec/html_spec.rb +0 -308
- data.tar.gz.sig +0 -0
- metadata.gz.sig +0 -1
data/spec/xpath_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
require 'nokogiri'
|
@@ -14,23 +16,23 @@ describe XPath do
|
|
14
16
|
let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
|
15
17
|
let(:doc) { Nokogiri::HTML(template) }
|
16
18
|
|
17
|
-
def xpath(type=nil, &block)
|
19
|
+
def xpath(type = nil, &block)
|
18
20
|
doc.xpath XPath.generate(&block).to_xpath(type)
|
19
21
|
end
|
20
22
|
|
21
|
-
it
|
23
|
+
it 'should work as a mixin' do
|
22
24
|
xpath = Thingy.new.foo_div.to_xpath
|
23
|
-
doc.xpath(xpath).first[:title].should
|
25
|
+
doc.xpath(xpath).first[:title].should eq 'fooDiv'
|
24
26
|
end
|
25
27
|
|
26
28
|
describe '#descendant' do
|
27
|
-
it
|
29
|
+
it 'should find nodes that are nested below the current node' do
|
28
30
|
@results = xpath { |x| x.descendant(:p) }
|
29
|
-
@results[0].text.should
|
30
|
-
@results[1].text.should
|
31
|
+
@results[0].text.should eq 'Blah'
|
32
|
+
@results[1].text.should eq 'Bax'
|
31
33
|
end
|
32
34
|
|
33
|
-
it
|
35
|
+
it 'should not find nodes outside the context' do
|
34
36
|
@results = xpath do |x|
|
35
37
|
foo_div = x.descendant(:div).where(x.attr(:id) == 'foo')
|
36
38
|
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
|
@@ -38,312 +40,523 @@ describe XPath do
|
|
38
40
|
@results[0].should be_nil
|
39
41
|
end
|
40
42
|
|
41
|
-
it
|
43
|
+
it 'should find multiple kinds of nodes' do
|
42
44
|
@results = xpath { |x| x.descendant(:p, :ul) }
|
43
|
-
@results[0].text.should
|
44
|
-
@results[3].text.should
|
45
|
+
@results[0].text.should eq 'Blah'
|
46
|
+
@results[3].text.should eq 'A list'
|
45
47
|
end
|
46
48
|
|
47
|
-
it
|
49
|
+
it 'should find all nodes when no arguments given' do
|
48
50
|
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].descendant }
|
49
|
-
@results[0].text.should
|
50
|
-
@results[4].text.should
|
51
|
+
@results[0].text.should eq 'Blah'
|
52
|
+
@results[4].text.should eq 'A list'
|
51
53
|
end
|
52
54
|
end
|
53
55
|
|
54
56
|
describe '#child' do
|
55
|
-
it
|
57
|
+
it 'should find nodes that are nested directly below the current node' do
|
56
58
|
@results = xpath { |x| x.descendant(:div).child(:p) }
|
57
|
-
@results[0].text.should
|
58
|
-
@results[1].text.should
|
59
|
+
@results[0].text.should eq 'Blah'
|
60
|
+
@results[1].text.should eq 'Bax'
|
59
61
|
end
|
60
62
|
|
61
|
-
it
|
63
|
+
it 'should not find nodes that are nested further down below the current node' do
|
62
64
|
@results = xpath { |x| x.child(:p) }
|
63
65
|
@results[0].should be_nil
|
64
66
|
end
|
65
67
|
|
66
|
-
it
|
68
|
+
it 'should find multiple kinds of nodes' do
|
67
69
|
@results = xpath { |x| x.descendant(:div).child(:p, :ul) }
|
68
|
-
@results[0].text.should
|
69
|
-
@results[3].text.should
|
70
|
+
@results[0].text.should eq 'Blah'
|
71
|
+
@results[3].text.should eq 'A list'
|
70
72
|
end
|
71
73
|
|
72
|
-
it
|
74
|
+
it 'should find all nodes when no arguments given' do
|
73
75
|
@results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].child }
|
74
|
-
@results[0].text.should
|
75
|
-
@results[3].text.should
|
76
|
+
@results[0].text.should eq 'Blah'
|
77
|
+
@results[3].text.should eq 'A list'
|
76
78
|
end
|
77
79
|
end
|
78
80
|
|
79
81
|
describe '#axis' do
|
80
|
-
it
|
82
|
+
it 'should find nodes given the xpath axis' do
|
81
83
|
@results = xpath { |x| x.axis(:descendant, :p) }
|
82
|
-
@results[0].text.should
|
84
|
+
@results[0].text.should eq 'Blah'
|
83
85
|
end
|
84
86
|
|
85
|
-
it
|
87
|
+
it 'should find nodes given the xpath axis without a specific tag' do
|
86
88
|
@results = xpath { |x| x.descendant(:div)[x.attr(:id) == 'foo'].axis(:descendant) }
|
87
|
-
@results[0][:id].should
|
89
|
+
@results[0][:id].should eq 'fooDiv'
|
88
90
|
end
|
89
91
|
end
|
90
92
|
|
91
93
|
describe '#next_sibling' do
|
92
|
-
it
|
93
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should
|
94
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should
|
95
|
-
xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should
|
94
|
+
it 'should find nodes which are immediate siblings of the current node' do
|
95
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should eq 'Bax'
|
96
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should eq 'Bax'
|
97
|
+
xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should eq 'A list'
|
96
98
|
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil
|
97
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should
|
99
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should eq 'Bax'
|
98
100
|
end
|
99
101
|
end
|
100
102
|
|
101
103
|
describe '#previous_sibling' do
|
102
|
-
it
|
103
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:p) }.first.text.should
|
104
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :p) }.first.text.should
|
105
|
-
xpath { |x| x.descendant(:p)[x.attr(:title) == 'gorilla'].previous_sibling(:ul, :p) }.first.text.should
|
104
|
+
it 'should find nodes which are exactly preceding the current node' do
|
105
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:p) }.first.text.should eq 'Bax'
|
106
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :p) }.first.text.should eq 'Bax'
|
107
|
+
xpath { |x| x.descendant(:p)[x.attr(:title) == 'gorilla'].previous_sibling(:ul, :p) }.first.text.should eq 'A list'
|
106
108
|
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :li) }.first.should be_nil
|
107
|
-
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling }.first.text.should
|
109
|
+
xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling }.first.text.should eq 'Bax'
|
108
110
|
end
|
109
111
|
end
|
110
112
|
|
111
113
|
describe '#anywhere' do
|
112
|
-
it
|
114
|
+
it 'should find nodes regardless of the context' do
|
113
115
|
@results = xpath do |x|
|
114
116
|
foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
|
115
117
|
x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
|
116
118
|
end
|
117
|
-
@results[0].text.should
|
119
|
+
@results[0].text.should eq 'Blah'
|
118
120
|
end
|
119
121
|
|
120
|
-
it
|
122
|
+
it 'should find multiple kinds of nodes regardless of the context' do
|
121
123
|
@results = xpath do |x|
|
122
|
-
context=x.descendant(:div).where(x.attr(:id)=='woo')
|
124
|
+
context = x.descendant(:div).where(x.attr(:id) == 'woo')
|
123
125
|
context.anywhere(:p, :ul)
|
124
126
|
end
|
125
127
|
|
126
|
-
@results[0].text.should
|
127
|
-
@results[3].text.should
|
128
|
-
@results[4].text.should
|
129
|
-
@results[6].text.should
|
128
|
+
@results[0].text.should eq 'Blah'
|
129
|
+
@results[3].text.should eq 'A list'
|
130
|
+
@results[4].text.should eq 'A list'
|
131
|
+
@results[6].text.should eq 'Bax'
|
130
132
|
end
|
131
133
|
|
132
|
-
it
|
134
|
+
it 'should find all nodes when no arguments given regardless of the context' do
|
133
135
|
@results = xpath do |x|
|
134
|
-
context=x.descendant(:div).where(x.attr(:id)=='woo')
|
136
|
+
context = x.descendant(:div).where(x.attr(:id) == 'woo')
|
135
137
|
context.anywhere
|
136
138
|
end
|
137
|
-
@results[0].name.should
|
138
|
-
@results[1].name.should
|
139
|
-
@results[2].name.should
|
140
|
-
@results[6].text.should
|
141
|
-
@results[10].text.should
|
142
|
-
@results[13].text.should
|
143
|
-
@results[15].text.should
|
139
|
+
@results[0].name.should eq 'html'
|
140
|
+
@results[1].name.should eq 'head'
|
141
|
+
@results[2].name.should eq 'body'
|
142
|
+
@results[6].text.should eq 'Blah'
|
143
|
+
@results[10].text.should eq 'A list'
|
144
|
+
@results[13].text.should eq 'A list'
|
145
|
+
@results[15].text.should eq 'Bax'
|
144
146
|
end
|
145
|
-
|
146
147
|
end
|
147
148
|
|
148
149
|
describe '#contains' do
|
149
|
-
it
|
150
|
+
it 'should find nodes that contain the given string' do
|
150
151
|
@results = xpath do |x|
|
151
152
|
x.descendant(:div).where(x.attr(:title).contains('ooD'))
|
152
153
|
end
|
153
|
-
@results[0][:id].should
|
154
|
+
@results[0][:id].should eq 'foo'
|
154
155
|
end
|
155
156
|
|
156
|
-
it
|
157
|
+
it 'should find nodes that contain the given expression' do
|
157
158
|
@results = xpath do |x|
|
158
159
|
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
|
159
160
|
x.descendant(:div).where(x.attr(:title).contains(expression))
|
160
161
|
end
|
161
|
-
@results[0][:id].should
|
162
|
+
@results[0][:id].should eq 'foo'
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe '#contains_word' do
|
167
|
+
it 'should find nodes that contain the given word in its entirety' do
|
168
|
+
@results = xpath do |x|
|
169
|
+
x.descendant.where(x.attr(:class).contains_word('fish'))
|
170
|
+
end
|
171
|
+
@results[0].text.should eq 'Bax'
|
172
|
+
@results[1].text.should eq 'llama'
|
173
|
+
@results.length.should eq 2
|
162
174
|
end
|
163
175
|
end
|
164
176
|
|
165
177
|
describe '#starts_with' do
|
166
|
-
it
|
178
|
+
it 'should find nodes that begin with the given string' do
|
167
179
|
@results = xpath do |x|
|
168
180
|
x.descendant(:*).where(x.attr(:id).starts_with('foo'))
|
169
181
|
end
|
170
|
-
@results.size.should
|
171
|
-
@results[0][:id].should
|
172
|
-
@results[1][:id].should
|
182
|
+
@results.size.should eq 2
|
183
|
+
@results[0][:id].should eq 'foo'
|
184
|
+
@results[1][:id].should eq 'fooDiv'
|
173
185
|
end
|
174
186
|
|
175
|
-
it
|
187
|
+
it 'should find nodes that contain the given expression' do
|
176
188
|
@results = xpath do |x|
|
177
189
|
expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
|
178
190
|
x.descendant(:div).where(x.attr(:title).starts_with(expression))
|
179
191
|
end
|
180
|
-
@results[0][:id].should
|
192
|
+
@results[0][:id].should eq 'foo'
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe '#ends_with' do
|
197
|
+
it 'should find nodes that end with the given string' do
|
198
|
+
@results = xpath do |x|
|
199
|
+
x.descendant(:*).where(x.attr(:id).ends_with('oof'))
|
200
|
+
end
|
201
|
+
@results.size.should eq 2
|
202
|
+
@results[0][:id].should eq 'oof'
|
203
|
+
@results[1][:id].should eq 'viDoof'
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'should find nodes that contain the given expression' do
|
207
|
+
@results = xpath do |x|
|
208
|
+
expression = x.anywhere(:div).where(x.attr(:title) == 'viDoof').attr(:id)
|
209
|
+
x.descendant(:div).where(x.attr(:title).ends_with(expression))
|
210
|
+
end
|
211
|
+
@results[0][:id].should eq 'oof'
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
describe '#uppercase' do
|
216
|
+
it 'should match uppercased text' do
|
217
|
+
@results = xpath do |x|
|
218
|
+
x.descendant(:div).where(x.attr(:title).uppercase == 'VIDOOF')
|
219
|
+
end
|
220
|
+
@results[0][:id].should eq 'oof'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
describe '#lowercase' do
|
225
|
+
it 'should match lowercased text' do
|
226
|
+
@results = xpath do |x|
|
227
|
+
x.descendant(:div).where(x.attr(:title).lowercase == 'vidoof')
|
228
|
+
end
|
229
|
+
@results[0][:id].should eq 'oof'
|
181
230
|
end
|
182
231
|
end
|
183
232
|
|
184
233
|
describe '#text' do
|
185
234
|
it "should select a node's text" do
|
186
235
|
@results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') }
|
187
|
-
@results[0].text.should
|
188
|
-
@results[1][:title].should
|
236
|
+
@results[0].text.should eq 'Bax'
|
237
|
+
@results[1][:title].should eq 'monkey'
|
189
238
|
@results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') }
|
190
|
-
@results[0][:title].should
|
239
|
+
@results[0][:title].should eq 'fooDiv'
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
describe '#substring' do
|
244
|
+
context 'when called with one argument' do
|
245
|
+
it 'should select the part of a string after the specified character' do
|
246
|
+
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'substring').text.substring(7) }
|
247
|
+
@results.should eq 'there'
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'when called with two arguments' do
|
252
|
+
it 'should select the part of a string after the specified character, up to the given length' do
|
253
|
+
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'substring').text.substring(2, 4) }
|
254
|
+
@results.should eq 'ello'
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
describe '#function' do
|
260
|
+
it 'should call the given xpath function' do
|
261
|
+
@results = xpath { |x| x.function(:boolean, x.function(:true) == x.function(:false)) }
|
262
|
+
@results.should eq false
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
describe '#method' do
|
267
|
+
it 'should call the given xpath function with the current node as the first argument' do
|
268
|
+
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'string-length').text.method(:"string-length") }
|
269
|
+
@results.should eq 11
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
describe '#string_length' do
|
274
|
+
it 'should return the length of a string' do
|
275
|
+
@results = xpath { |x| x.descendant(:span).where(x.attr(:id) == 'string-length').text.string_length }
|
276
|
+
@results.should eq 11
|
191
277
|
end
|
192
278
|
end
|
193
279
|
|
194
280
|
describe '#where' do
|
195
|
-
it
|
196
|
-
xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should
|
281
|
+
it 'should limit the expression to find only certain nodes' do
|
282
|
+
xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should eq 'fooDiv'
|
197
283
|
end
|
198
284
|
|
199
|
-
it
|
200
|
-
xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should
|
285
|
+
it 'should be aliased as []' do
|
286
|
+
xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should eq 'fooDiv'
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'should be a no-op when nil condition is passed' do
|
290
|
+
XPath.descendant(:div).where(nil).to_s.should eq './/div'
|
201
291
|
end
|
202
292
|
end
|
203
293
|
|
204
294
|
describe '#inverse' do
|
205
|
-
it
|
206
|
-
xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should
|
295
|
+
it 'should invert the expression' do
|
296
|
+
xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should eq 'Bax'
|
207
297
|
end
|
208
298
|
|
209
|
-
it
|
210
|
-
xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should
|
299
|
+
it 'should be aliased as the unary tilde' do
|
300
|
+
xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should eq 'Bax'
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'should be aliased as the unary bang' do
|
304
|
+
xpath { |x| x.descendant(:p).where(!x.attr(:id).equals('fooDiv')) }.first.text.should eq 'Bax'
|
211
305
|
end
|
212
306
|
end
|
213
307
|
|
214
308
|
describe '#equals' do
|
215
|
-
it
|
216
|
-
xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should
|
309
|
+
it 'should limit the expression to find only certain nodes' do
|
310
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should eq 'fooDiv'
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'should be aliased as ==' do
|
314
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should eq 'fooDiv'
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe '#not_equals' do
|
319
|
+
it 'should match only when not equal' do
|
320
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).not_equals('bar')) }.first[:title].should eq 'fooDiv'
|
217
321
|
end
|
218
322
|
|
219
|
-
it
|
220
|
-
xpath { |x| x.descendant(:div).where(x.attr(:id)
|
323
|
+
it 'should be aliased as !=' do
|
324
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id) != 'bar') }.first[:title].should eq 'fooDiv'
|
221
325
|
end
|
222
326
|
end
|
223
327
|
|
224
328
|
describe '#is' do
|
225
|
-
it
|
226
|
-
xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should
|
329
|
+
it 'uses equality when :exact given' do
|
330
|
+
xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should eq 'fooDiv'
|
227
331
|
xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first.should be_nil
|
228
332
|
end
|
229
333
|
|
230
|
-
it
|
231
|
-
xpath { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should
|
232
|
-
xpath { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first[:title].should
|
334
|
+
it 'uses substring matching otherwise' do
|
335
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should eq 'fooDiv'
|
336
|
+
xpath { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first[:title].should eq 'fooDiv'
|
233
337
|
end
|
234
338
|
end
|
235
339
|
|
236
340
|
describe '#one_of' do
|
237
|
-
it
|
341
|
+
it 'should return all nodes where the condition matches' do
|
238
342
|
@results = xpath do |x|
|
239
343
|
p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title)
|
240
344
|
x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz'))
|
241
345
|
end
|
242
|
-
@results[0][:title].should
|
243
|
-
@results[1].text.should
|
244
|
-
@results[2][:title].should
|
346
|
+
@results[0][:title].should eq 'fooDiv'
|
347
|
+
@results[1].text.should eq 'Blah'
|
348
|
+
@results[2][:title].should eq 'bazDiv'
|
245
349
|
end
|
246
350
|
end
|
247
351
|
|
248
352
|
describe '#and' do
|
249
|
-
it
|
353
|
+
it 'should find all nodes in both expression' do
|
250
354
|
@results = xpath do |x|
|
251
355
|
x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey')))
|
252
356
|
end
|
253
|
-
@results[0][:title].should
|
357
|
+
@results[0][:title].should eq 'monkey'
|
254
358
|
end
|
255
359
|
|
256
|
-
it
|
360
|
+
it 'should be aliased as ampersand (&)' do
|
257
361
|
@results = xpath do |x|
|
258
362
|
x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey'))
|
259
363
|
end
|
260
|
-
@results[0][:title].should
|
364
|
+
@results[0][:title].should eq 'monkey'
|
261
365
|
end
|
262
366
|
end
|
263
367
|
|
264
368
|
describe '#or' do
|
265
|
-
it
|
369
|
+
it 'should find all nodes in either expression' do
|
266
370
|
@results = xpath do |x|
|
267
371
|
x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv')))
|
268
372
|
end
|
269
|
-
@results[0][:title].should
|
270
|
-
@results[1].text.should
|
373
|
+
@results[0][:title].should eq 'fooDiv'
|
374
|
+
@results[1].text.should eq 'Blah'
|
271
375
|
end
|
272
376
|
|
273
|
-
it
|
377
|
+
it 'should be aliased as pipe (|)' do
|
274
378
|
@results = xpath do |x|
|
275
379
|
x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv'))
|
276
380
|
end
|
277
|
-
@results[0][:title].should
|
278
|
-
@results[1].text.should
|
381
|
+
@results[0][:title].should eq 'fooDiv'
|
382
|
+
@results[1].text.should eq 'Blah'
|
279
383
|
end
|
280
384
|
end
|
281
385
|
|
282
386
|
describe '#attr' do
|
283
|
-
it
|
387
|
+
it 'should be an attribute' do
|
284
388
|
@results = xpath { |x| x.descendant(:div).where(x.attr(:id)) }
|
285
|
-
@results[0][:title].should
|
286
|
-
@results[1][:title].should
|
389
|
+
@results[0][:title].should eq 'barDiv'
|
390
|
+
@results[1][:title].should eq 'fooDiv'
|
287
391
|
end
|
288
392
|
end
|
289
393
|
|
290
394
|
describe '#css' do
|
291
|
-
it
|
395
|
+
it 'should find nodes by the given CSS selector' do
|
292
396
|
@results = xpath { |x| x.css('#preference p') }
|
293
|
-
@results[0].text.should
|
294
|
-
@results[1].text.should
|
397
|
+
@results[0].text.should eq 'allamas'
|
398
|
+
@results[1].text.should eq 'llama'
|
295
399
|
end
|
296
400
|
|
297
|
-
it
|
401
|
+
it 'should respect previous expression' do
|
298
402
|
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('p') }
|
299
|
-
@results[0].text.should
|
300
|
-
@results[1].text.should
|
403
|
+
@results[0].text.should eq 'chimp'
|
404
|
+
@results[1].text.should eq 'flamingo'
|
301
405
|
end
|
302
406
|
|
303
|
-
it
|
407
|
+
it 'should be composable' do
|
304
408
|
@results = xpath { |x| x.css('#moar').descendant(:p) }
|
305
|
-
@results[0].text.should
|
306
|
-
@results[1].text.should
|
409
|
+
@results[0].text.should eq 'chimp'
|
410
|
+
@results[1].text.should eq 'flamingo'
|
307
411
|
end
|
308
412
|
|
309
|
-
it
|
413
|
+
it 'should allow comma separated selectors' do
|
310
414
|
@results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('div, p') }
|
311
|
-
@results[0].text.should
|
312
|
-
@results[1].text.should
|
313
|
-
@results[2].text.should
|
415
|
+
@results[0].text.should eq 'chimp'
|
416
|
+
@results[1].text.should eq 'elephant'
|
417
|
+
@results[2].text.should eq 'flamingo'
|
314
418
|
end
|
315
419
|
end
|
316
420
|
|
317
|
-
describe '#
|
421
|
+
describe '#qname' do
|
318
422
|
it "should match the node's name" do
|
319
|
-
xpath { |x| x.descendant(:*).where(x.
|
423
|
+
xpath { |x| x.descendant(:*).where(x.qname == 'ul') }.first.text.should eq 'A list'
|
320
424
|
end
|
321
425
|
end
|
322
426
|
|
323
427
|
describe '#union' do
|
324
|
-
it
|
428
|
+
it 'should create a union expression' do
|
325
429
|
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
326
430
|
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
327
431
|
@collection = @expr1.union(@expr2)
|
328
432
|
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
329
433
|
@xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath
|
330
434
|
@results = doc.xpath(@xpath1)
|
331
|
-
@results[0][:title].should
|
435
|
+
@results[0][:title].should eq 'fooDiv'
|
332
436
|
@results = doc.xpath(@xpath2)
|
333
|
-
@results[0][:id].should
|
437
|
+
@results[0][:id].should eq 'fooDiv'
|
334
438
|
end
|
335
439
|
|
336
|
-
it
|
440
|
+
it 'should be aliased as +' do
|
337
441
|
@expr1 = XPath.generate { |x| x.descendant(:p) }
|
338
442
|
@expr2 = XPath.generate { |x| x.descendant(:div) }
|
339
443
|
@collection = @expr1 + @expr2
|
340
444
|
@xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
|
341
445
|
@xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath
|
342
446
|
@results = doc.xpath(@xpath1)
|
343
|
-
@results[0][:title].should
|
447
|
+
@results[0][:title].should eq 'fooDiv'
|
344
448
|
@results = doc.xpath(@xpath2)
|
345
|
-
@results[0][:id].should
|
449
|
+
@results[0][:id].should eq 'fooDiv'
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
describe '#last' do
|
454
|
+
it 'returns the number of elements in the context' do
|
455
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() == XPath.last()] }
|
456
|
+
@results[0].text.should eq 'Bax'
|
457
|
+
@results[1].text.should eq 'Blah'
|
458
|
+
@results[2].text.should eq 'llama'
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
describe '#position' do
|
463
|
+
it 'returns the position of elements in the context' do
|
464
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() == 2] }
|
465
|
+
@results[0].text.should eq 'Bax'
|
466
|
+
@results[1].text.should eq 'Bax'
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
describe '#count' do
|
471
|
+
it 'counts the number of occurrences' do
|
472
|
+
@results = xpath { |x| x.descendant(:div)[x.descendant(:p).count == 2] }
|
473
|
+
@results[0][:id].should eq 'preference'
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
describe '#lte' do
|
478
|
+
it 'checks lesser than or equal' do
|
479
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() <= 2] }
|
480
|
+
@results[0].text.should eq 'Blah'
|
481
|
+
@results[1].text.should eq 'Bax'
|
482
|
+
@results[2][:title].should eq 'gorilla'
|
483
|
+
@results[3].text.should eq 'Bax'
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
describe '#lt' do
|
488
|
+
it 'checks lesser than' do
|
489
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() < 2] }
|
490
|
+
@results[0].text.should eq 'Blah'
|
491
|
+
@results[1][:title].should eq 'gorilla'
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
describe '#gte' do
|
496
|
+
it 'checks greater than or equal' do
|
497
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() >= 2] }
|
498
|
+
@results[0].text.should eq 'Bax'
|
499
|
+
@results[1][:title].should eq 'monkey'
|
500
|
+
@results[2].text.should eq 'Bax'
|
501
|
+
@results[3].text.should eq 'Blah'
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
describe '#gt' do
|
506
|
+
it 'checks greater than' do
|
507
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() > 2] }
|
508
|
+
@results[0][:title].should eq 'monkey'
|
509
|
+
@results[1].text.should eq 'Blah'
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
describe '#plus' do
|
514
|
+
it 'adds stuff' do
|
515
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position().plus(1) == 2] }
|
516
|
+
@results[0][:id].should eq 'fooDiv'
|
517
|
+
@results[1][:title].should eq 'gorilla'
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
describe '#minus' do
|
522
|
+
it 'subtracts stuff' do
|
523
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position().minus(1) == 0] }
|
524
|
+
@results[0][:id].should eq 'fooDiv'
|
525
|
+
@results[1][:title].should eq 'gorilla'
|
346
526
|
end
|
347
527
|
end
|
348
528
|
|
529
|
+
describe '#multiply' do
|
530
|
+
it 'multiplies stuff' do
|
531
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() * 3 == 3] }
|
532
|
+
@results[0][:id].should eq 'fooDiv'
|
533
|
+
@results[1][:title].should eq 'gorilla'
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
describe '#divide' do
|
538
|
+
it 'divides stuff' do
|
539
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() / 2 == 1] }
|
540
|
+
@results[0].text.should eq 'Bax'
|
541
|
+
@results[1].text.should eq 'Bax'
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
describe '#mod' do
|
546
|
+
it 'take modulo' do
|
547
|
+
@results = xpath { |x| x.descendant(:p)[XPath.position() % 2 == 1] }
|
548
|
+
@results[0].text.should eq 'Blah'
|
549
|
+
@results[1][:title].should eq 'monkey'
|
550
|
+
@results[2][:title].should eq 'gorilla'
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
describe '#ancestor' do
|
555
|
+
it 'finds ancestor nodes' do
|
556
|
+
@results = xpath { |x| x.descendant(:p)[1].ancestor }
|
557
|
+
@results[0].node_name.should eq 'html'
|
558
|
+
@results[1].node_name.should eq 'body'
|
559
|
+
@results[2][:id].should eq 'foo'
|
560
|
+
end
|
561
|
+
end
|
349
562
|
end
|