wgibbs-xpath 0.1.4

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