wgibbs-xpath 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,353 @@
1
+ require 'spec_helper'
2
+
3
+ require 'nokogiri'
4
+
5
+ class Thingy
6
+ include XPath
7
+
8
+ def foo_div
9
+ descendant(:div).where(attr(:id) == 'foo')
10
+ end
11
+ end
12
+
13
+ describe XPath do
14
+ let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) }
15
+ let(:doc) { Nokogiri::HTML(template) }
16
+
17
+ def xpath(predicate=nil, &block)
18
+ doc.xpath XPath.generate(&block).to_xpath(predicate)
19
+ end
20
+
21
+ it "should work as a mixin" do
22
+ xpath = Thingy.new.foo_div.to_xpath
23
+ doc.xpath(xpath).first[:title].should == 'fooDiv'
24
+ end
25
+
26
+ describe '#descendant' do
27
+ it "should find nodes that are nested below the current node" do
28
+ @results = xpath { |x| x.descendant(:p) }
29
+ @results[0].text.should == "Blah"
30
+ @results[1].text.should == "Bax"
31
+ end
32
+
33
+ it "should not find nodes outside the context" do
34
+ @results = xpath do |x|
35
+ foo_div = x.descendant(:div).where(x.attr(:id) == 'foo')
36
+ x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
37
+ end
38
+ @results[0].should be_nil
39
+ end
40
+
41
+ it "should find multiple kinds of nodes" do
42
+ @results = xpath { |x| x.descendant(:p, :ul) }
43
+ @results[0].text.should == 'Blah'
44
+ @results[3].text.should == 'A list'
45
+ end
46
+
47
+ it "should find all nodes when no arguments given" do
48
+ @results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].descendant }
49
+ @results[0].text.should == 'Blah'
50
+ @results[4].text.should == 'A list'
51
+ end
52
+ end
53
+
54
+ describe '#child' do
55
+ it "should find nodes that are nested directly below the current node" do
56
+ @results = xpath { |x| x.descendant(:div).child(:p) }
57
+ @results[0].text.should == "Blah"
58
+ @results[1].text.should == "Bax"
59
+ end
60
+
61
+ it "should not find nodes that are nested further down below the current node" do
62
+ @results = xpath { |x| x.child(:p) }
63
+ @results[0].should be_nil
64
+ end
65
+
66
+ it "should find multiple kinds of nodes" do
67
+ @results = xpath { |x| x.descendant(:div).child(:p, :ul) }
68
+ @results[0].text.should == 'Blah'
69
+ @results[3].text.should == 'A list'
70
+ end
71
+
72
+ it "should find all nodes when no arguments given" do
73
+ @results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].child }
74
+ @results[0].text.should == 'Blah'
75
+ @results[3].text.should == 'A list'
76
+ end
77
+ end
78
+
79
+ describe '#next_sibling' do
80
+ it "should find nodes which are immediate siblings of the current node" do
81
+ xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should == 'Bax'
82
+ xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should == 'Bax'
83
+ xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should == 'A list'
84
+ xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil
85
+ xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should == 'Bax'
86
+ end
87
+ end
88
+
89
+ describe '#anywhere' do
90
+ it "should find nodes regardless of the context" do
91
+ @results = xpath do |x|
92
+ foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
93
+ x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title))
94
+ end
95
+ @results[0].text.should == "Blah"
96
+ end
97
+ end
98
+
99
+ describe '#tag' do
100
+ it "should filter elements by tag" do
101
+ @results = xpath { |x| x.descendant[x.tag(:p) | x.tag(:li)] }
102
+ @results[0].text.should == 'Blah'
103
+ @results[3].text.should == 'A list'
104
+ end
105
+ end
106
+
107
+ describe '#contains' do
108
+ it "should find nodes that contain the given string" do
109
+ @results = xpath do |x|
110
+ x.descendant(:div).where(x.attr(:title).contains('ooD'))
111
+ end
112
+ @results[0][:id].should == "foo"
113
+ end
114
+
115
+ it "should find nodes that contain the given expression" do
116
+ @results = xpath do |x|
117
+ expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id)
118
+ x.descendant(:div).where(x.attr(:title).contains(expression))
119
+ end
120
+ @results[0][:id].should == "foo"
121
+ end
122
+ end
123
+
124
+ describe '#text' do
125
+ it "should select a node's text" do
126
+ @results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') }
127
+ @results[0].text.should == 'Bax'
128
+ @results[1][:title].should == 'monkey'
129
+ @results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') }
130
+ @results[0][:title].should == 'fooDiv'
131
+ end
132
+ end
133
+
134
+ describe '#where' do
135
+ it "should limit the expression to find only certain nodes" do
136
+ xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should == "fooDiv"
137
+ end
138
+
139
+ it "should be aliased as []" do
140
+ xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should == "fooDiv"
141
+ end
142
+ end
143
+
144
+ describe '#inverse' do
145
+ it "should invert the expression" do
146
+ xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should == 'Bax'
147
+ end
148
+
149
+ it "should be aliased as the unary tilde" do
150
+ xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should == 'Bax'
151
+ end
152
+ end
153
+
154
+ describe '#equals' do
155
+ it "should limit the expression to find only certain nodes" do
156
+ xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should == "fooDiv"
157
+ end
158
+
159
+ it "should be aliased as ==" do
160
+ xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should == "fooDiv"
161
+ end
162
+ end
163
+
164
+ describe '#is' do
165
+ it "should limit the expression to only nodes that contain the given expression" do
166
+ @results = xpath { |x| x.descendant(:p).where(x.text.is('llama')) }
167
+ @results[0][:id].should == 'is-fuzzy'
168
+ @results[1][:id].should == 'is-exact'
169
+ end
170
+
171
+ it "should limit the expression to only nodes that contain the given expression if fuzzy predicate given" do
172
+ @results = xpath(:fuzzy) { |x| x.descendant(:p).where(x.text.is('llama')) }
173
+ @results[0][:id].should == 'is-fuzzy'
174
+ @results[1][:id].should == 'is-exact'
175
+ end
176
+
177
+ it "should limit the expression to only nodes that equal the given expression if exact predicate given" do
178
+ @results = xpath(:exact) { |x| x.descendant(:p).where(x.text.is('llama')) }
179
+ @results[0][:id].should == 'is-exact'
180
+ @results[1].should be_nil
181
+ end
182
+
183
+ context "with to_xpaths" do
184
+ it "should prefer exact matches" do
185
+ @xpath = XPath.generate { |x| x.descendant(:p).where(x.text.is('llama')) }
186
+ @results = @xpath.to_xpaths.map { |path| doc.xpath(path) }.flatten
187
+ @results[0][:id].should == 'is-exact'
188
+ @results[1][:id].should == 'is-fuzzy'
189
+ end
190
+ end
191
+ end
192
+
193
+ describe '#one_of' do
194
+ it "should return all nodes where the condition matches" do
195
+ @results = xpath do |x|
196
+ p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title)
197
+ x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz'))
198
+ end
199
+ @results[0][:title].should == "fooDiv"
200
+ @results[1].text.should == "Blah"
201
+ @results[2][:title].should == "bazDiv"
202
+ end
203
+ end
204
+
205
+ describe '#and' do
206
+ it "should find all nodes in both expression" do
207
+ @results = xpath do |x|
208
+ x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey')))
209
+ end
210
+ @results[0][:title].should == "monkey"
211
+ end
212
+
213
+ it "should be aliased as ampersand (&)" do
214
+ @results = xpath do |x|
215
+ x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey'))
216
+ end
217
+ @results[0][:title].should == "monkey"
218
+ end
219
+ end
220
+
221
+ describe '#or' do
222
+ it "should find all nodes in either expression" do
223
+ @results = xpath do |x|
224
+ x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv')))
225
+ end
226
+ @results[0][:title].should == "fooDiv"
227
+ @results[1].text.should == "Blah"
228
+ end
229
+
230
+ it "should be aliased as pipe (|)" do
231
+ @results = xpath do |x|
232
+ x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv'))
233
+ end
234
+ @results[0][:title].should == "fooDiv"
235
+ @results[1].text.should == "Blah"
236
+ end
237
+ end
238
+
239
+ describe '#attr' do
240
+ it "should be an attribute" do
241
+ @results = xpath { |x| x.descendant(:div).where(x.attr(:id)) }
242
+ @results[0][:title].should == "barDiv"
243
+ @results[1][:title].should == "fooDiv"
244
+ end
245
+
246
+ it "should be closed" do
247
+ @results = xpath do |x|
248
+ foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo')
249
+ id = x.attr(foo_div.attr(:data))
250
+ x.descendant(:div).where(id == 'bar')
251
+ end.first[:title].should == "barDiv"
252
+ end
253
+ end
254
+
255
+ describe '#css' do
256
+ it "should find nodes by the given CSS selector" do
257
+ @results = xpath { |x| x.css('#preference p') }
258
+ @results[0].text.should == 'allamas'
259
+ @results[1].text.should == 'llama'
260
+ end
261
+
262
+ it "should respect previous expression" do
263
+ @results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('p') }
264
+ @results[0].text.should == 'chimp'
265
+ @results[1].text.should == 'flamingo'
266
+ end
267
+
268
+ it "should be composable" do
269
+ @results = xpath { |x| x.css('#moar').descendant(:p) }
270
+ @results[0].text.should == 'chimp'
271
+ @results[1].text.should == 'flamingo'
272
+ end
273
+
274
+ it "should allow comma separated selectors" do
275
+ @results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('div, p') }
276
+ @results[0].text.should == 'chimp'
277
+ @results[1].text.should == 'elephant'
278
+ @results[2].text.should == 'flamingo'
279
+ end
280
+ end
281
+
282
+ describe '#name' do
283
+ it "should match the node's name" do
284
+ xpath { |x| x.descendant(:*).where(x.name == 'ul') }.first.text.should == "A list"
285
+ end
286
+ end
287
+
288
+ describe '#apply and #var' do
289
+ it "should interpolate variables in the xpath expression" do
290
+ @xpath = XPath.generate do |x|
291
+ exp = x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal)
292
+ end
293
+ @result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
294
+ @result1[:title].should == 'fooDiv'
295
+ @result2 = doc.xpath(@xpath.apply(:id => 'baz').to_xpath).first
296
+ @result2[:title].should == 'bazDiv'
297
+ end
298
+
299
+ it "should raise an argument error if the interpolation key is not given" do
300
+ @xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.var(:id).string_literal) }
301
+ if defined?(KeyError)
302
+ lambda { @xpath.apply.to_xpath }.should raise_error(KeyError)
303
+ else
304
+ lambda { @xpath.apply.to_xpath }.should raise_error(ArgumentError)
305
+ end
306
+ end
307
+ end
308
+
309
+ describe '#varstring' do
310
+ it "should add a literal string variable" do
311
+ @xpath = XPath.generate { |x| x.descendant(:*).where(x.attr(:id) == x.varstring(:id)) }
312
+ @result1 = doc.xpath(@xpath.apply(:id => 'foo').to_xpath).first
313
+ @result1[:title].should == 'fooDiv'
314
+ end
315
+ end
316
+
317
+ describe '#normalize' do
318
+ it "should normalize whitespace" do
319
+ xpath { |x| x.descendant(:p).where(x.text.normalize == 'A lot of whitespace') }.first[:id].should == "whitespace"
320
+ end
321
+
322
+ it "should be aliased as 'n'" do
323
+ xpath { |x| x.descendant(:p).where(x.text.n == 'A lot of whitespace') }.first[:id].should == "whitespace"
324
+ end
325
+ end
326
+
327
+ describe '#union' do
328
+ it "should create a union expression" do
329
+ @expr1 = XPath.generate { |x| x.descendant(:p) }
330
+ @expr2 = XPath.generate { |x| x.descendant(:div) }
331
+ @collection = @expr1.union(@expr2)
332
+ @xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
333
+ @xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
334
+ @results = doc.xpath(@xpath1)
335
+ @results[0][:title].should == 'fooDiv'
336
+ @results = doc.xpath(@xpath2)
337
+ @results[0][:id].should == 'fooDiv'
338
+ end
339
+
340
+ it "should be aliased as +" do
341
+ @expr1 = XPath.generate { |x| x.descendant(:p) }
342
+ @expr2 = XPath.generate { |x| x.descendant(:div) }
343
+ @collection = @expr1 + @expr2
344
+ @xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath
345
+ @xpath2 = @collection.where(XPath.attr(:id) == XPath.varstring(:id)).apply(:id => 'fooDiv').to_xpath
346
+ @results = doc.xpath(@xpath1)
347
+ @results[0][:title].should == 'fooDiv'
348
+ @results = doc.xpath(@xpath2)
349
+ @results[0][:id].should == 'fooDiv'
350
+ end
351
+ end
352
+
353
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wgibbs-xpath
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 4
10
+ version: 0.1.4
11
+ platform: ruby
12
+ authors:
13
+ - Wes Gibbs
14
+ - Jonas Nicklas
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2011-03-09 00:00:00 -05:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: nokogiri
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 9
31
+ segments:
32
+ - 1
33
+ - 3
34
+ version: "1.3"
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 13
46
+ segments:
47
+ - 1
48
+ - 2
49
+ - 9
50
+ version: 1.2.9
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: yard
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 27
62
+ segments:
63
+ - 0
64
+ - 5
65
+ - 8
66
+ version: 0.5.8
67
+ type: :development
68
+ version_requirements: *id003
69
+ description: XPath is a Ruby DSL for generating XPath expressions
70
+ email:
71
+ - wesgibbs@gmail.com
72
+ - jonas.nicklas@gmail.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files:
78
+ - README.rdoc
79
+ files:
80
+ - lib/xpath/expression.rb
81
+ - lib/xpath/html.rb
82
+ - lib/xpath/union.rb
83
+ - lib/xpath/version.rb
84
+ - lib/xpath.rb
85
+ - spec/fixtures/form.html
86
+ - spec/fixtures/simple.html
87
+ - spec/fixtures/stuff.html
88
+ - spec/html_spec.rb
89
+ - spec/spec_helper.rb
90
+ - spec/union_spec.rb
91
+ - spec/xpath_spec.rb
92
+ - README.rdoc
93
+ has_rdoc: true
94
+ homepage: http://github.com/wgibbs/xpath
95
+ licenses: []
96
+
97
+ post_install_message:
98
+ rdoc_options:
99
+ - --main
100
+ - README.rdoc
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 3
109
+ segments:
110
+ - 0
111
+ version: "0"
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ requirements: []
122
+
123
+ rubyforge_project: wgibbs-xpath
124
+ rubygems_version: 1.5.2
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: Generate XPath expressions from Ruby
128
+ test_files: []
129
+