pax_global_header00006660000000000000000000000064121472621650014520gustar00rootroot0000000000000052 comment=d152570e321276c352d6a24b4bad8291edb59190 ruby-xpath-2.0.0/000077500000000000000000000000001214726216500136225ustar00rootroot00000000000000ruby-xpath-2.0.0/README.md000066400000000000000000000077341214726216500151140ustar00rootroot00000000000000# XPath XPath is a Ruby DSL around a subset of XPath 1.0. Its primary purpose is to facilitate writing complex XPath queries from Ruby code. [![Build Status](https://secure.travis-ci.org/jnicklas/xpath.png?branch=master)](http://travis-ci.org/jnicklas/xpath) ## Generating expressions To create quick, one-off expressions, `XPath.generate` can be used: ``` ruby XPath.generate { |x| x.descendant(:ul)[x.attr(:id) == 'foo'] } ``` You can also call expression methods directly on the `XPath` module: ``` ruby XPath.descendant(:ul)[XPath.attr(:id) == 'foo'] ``` However for more complex expressions, it is probably more convenient to include the `XPath` module into your own class or module: ``` ruby module MyXPaths include XPath def foo_ul descendant(:ul)[attr(:id) == 'foo'] end def password_field(id) descendant(:input)[attr(:type) == 'password'][attr(:id) == id] end end ``` Both ways return an [`XPath::Expression`](http://rdoc.info/github/jnicklas/xpath/XPath/Expression) instance, which can be further modified. To convert the expression to a string, just call `#to_s` on it. All available expressions are defined in [`XPath::DSL`](http://rdoc.info/github/jnicklas/xpath/XPath/DSL). ## String, Hashes and Symbols When you send a string as an argument to any XPath function, XPath assumes this to be a string literal. On the other hand if you send in Symbol, XPath assumes this to be an XPath literal. Thus the following two statements are not equivalent: ``` ruby XPath.descendant(:p)[XPath.attr(:id) == 'foo'] XPath.descendant(:p)[XPath.attr(:id) == :foo] ``` These are the XPath expressions that these would be translated to: ``` .//p[@id = 'foo'] .//p[@id = foo] ``` The second expression would match any p tag whose id attribute matches a 'foo' tag it contains. Most likely this is not what you want. In fact anything other than a String is treated as a literal. Thus the following works as expected: ``` ruby XPath.descendant(:p)[1] ``` Keep in mind that XPath is 1-indexed and not 0-indexed like most other programming languages, including Ruby. Hashes are automatically converted to equality expressions, so the above example could be written as: ``` ruby XPath.descendant(:p)[:@id => 'foo'] ``` Which would generate the same expression: ``` .//p[@id = 'foo'] ``` Note that the same rules apply here, both the keys and values in the hash are treated the same way as any other expression in XPath. Thus the following are not equivalent: ``` ruby XPath.descendant(:p)[:@id => 'foo'] # => .//p[@id = 'foo'] XPath.descendant(:p)[:id => 'foo'] # => .//p[id = 'foo'] XPath.descendant(:p)['id' => 'foo'] # => .//p['id' = 'foo'] ``` ## HTML XPath comes with a set of premade XPaths for use with HTML documents. You can generate these like this: ``` ruby XPath::HTML.link('Home') XPath::HTML.field('Name') ``` See [`XPath::HTML`](http://rdoc.info/github/jnicklas/xpath/XPath/HTML) for all available matchers. ## License (The MIT License) Copyright © 2010 Jonas Nicklas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ruby-xpath-2.0.0/data.tar.gz.sig000066400000000000000000000004001214726216500164350ustar00rootroot00000000000000@N95rnM G'wVʘig7&&VƘYH Po[ A0-Lcr53 I兛C&zJHG'zBSiR} ~g:vOu QFC>FJ{2JŒobTpnfVW;z8~-ӳM h_j-g;#y@ mw#'m̗M)oU[fzfruby-xpath-2.0.0/lib/000077500000000000000000000000001214726216500143705ustar00rootroot00000000000000ruby-xpath-2.0.0/lib/xpath.rb000066400000000000000000000004321214726216500160400ustar00rootroot00000000000000require 'nokogiri' require 'xpath/dsl' require 'xpath/expression' require 'xpath/literal' require 'xpath/union' require 'xpath/renderer' require 'xpath/html' module XPath extend XPath::DSL::TopLevel include XPath::DSL::TopLevel def self.generate yield(self) end end ruby-xpath-2.0.0/lib/xpath/000077500000000000000000000000001214726216500155145ustar00rootroot00000000000000ruby-xpath-2.0.0/lib/xpath/dsl.rb000066400000000000000000000046731214726216500166350ustar00rootroot00000000000000module XPath module DSL module TopLevel def current Expression.new(:this_node) end def name Expression.new(:node_name, current) end def descendant(*expressions) Expression.new(:descendant, current, expressions) end def child(*expressions) Expression.new(:child, current, expressions) end def axis(name, tag_name=:*) Expression.new(:axis, current, name, tag_name) end def next_sibling(*expressions) Expression.new(:next_sibling, current, expressions) end def previous_sibling(*expressions) Expression.new(:previous_sibling, current, expressions) end def anywhere(*expressions) Expression.new(:anywhere, expressions) end def attr(expression) Expression.new(:attribute, current, expression) end def contains(expression) Expression.new(:contains, current, expression) end def starts_with(expression) Expression.new(:starts_with, current, expression) end def text Expression.new(:text, current) end def string Expression.new(:string_function, current) end def css(selector) Expression.new(:css, current, Literal.new(selector)) end end module ExpressionLevel include XPath::DSL::TopLevel def where(expression) Expression.new(:where, current, expression) end alias_method :[], :where def one_of(*expressions) Expression.new(:one_of, current, expressions) end def equals(expression) Expression.new(:equality, current, expression) end alias_method :==, :equals def is(expression) Expression.new(:is, current, expression) end def or(expression) Expression.new(:or, current, expression) end alias_method :|, :or def and(expression) Expression.new(:and, current, expression) end alias_method :&, :and def union(*expressions) Union.new(*[self, expressions].flatten) end alias_method :+, :union def inverse Expression.new(:inverse, current) end alias_method :~, :inverse def string_literal Expression.new(:string_literal, self) end def normalize Expression.new(:normalized_space, current) end alias_method :n, :normalize end end end ruby-xpath-2.0.0/lib/xpath/expression.rb000066400000000000000000000005701214726216500202420ustar00rootroot00000000000000module XPath class Expression attr_accessor :expression, :arguments include XPath::DSL::ExpressionLevel def initialize(expression, *arguments) @expression = expression @arguments = arguments end def current self end def to_xpath(type=nil) Renderer.render(self, type) end alias_method :to_s, :to_xpath end end ruby-xpath-2.0.0/lib/xpath/html.rb000066400000000000000000000120171214726216500170060ustar00rootroot00000000000000module XPath module HTML include XPath::DSL::TopLevel extend self # Match an `a` link element. # # @param [String] locator # Text, id, title, or image alt attribute of the link # def link(locator) locator = locator.to_s link = descendant(:a)[attr(:href)] link[attr(:id).equals(locator) | string.n.is(locator) | attr(:title).is(locator) | descendant(:img)[attr(:alt).is(locator)]] end # Match a `submit`, `image`, or `button` element. # # @param [String] locator # Value, title, id, or image alt attribute of the button # def button(locator) locator = locator.to_s button = descendant(:input)[attr(:type).one_of('submit', 'reset', 'image', 'button')][attr(:id).equals(locator) | attr(:value).is(locator) | attr(:title).is(locator)] button += descendant(:button)[attr(:id).equals(locator) | attr(:value).is(locator) | string.n.is(locator) | attr(:title).is(locator)] button += descendant(:input)[attr(:type).equals('image')][attr(:alt).is(locator)] end # Match anything returned by either {#link} or {#button}. # # @param [String] locator # Text, id, title, or image alt attribute of the link or button # def link_or_button(locator) link(locator) + button(locator) end # Match any `fieldset` element. # # @param [String] locator # Legend or id of the fieldset # def fieldset(locator) locator = locator.to_s descendant(:fieldset)[attr(:id).equals(locator) | child(:legend)[string.n.is(locator)]] end # Match any `input`, `textarea`, or `select` element that doesn't have a # type of `submit`, `image`, or `hidden`. # # @param [String] locator # Label, id, or name of field to match # def field(locator) locator = locator.to_s xpath = descendant(:input, :textarea, :select)[~attr(:type).one_of('submit', 'image', 'hidden')] xpath = locate_field(xpath, locator) xpath end # Match any `input` or `textarea` element that can be filled with text. # This excludes any inputs with a type of `submit`, `image`, `radio`, # `checkbox`, `hidden`, or `file`. # # @param [String] locator # Label, id, or name of field to match # def fillable_field(locator) locator = locator.to_s xpath = descendant(:input, :textarea)[~attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')] xpath = locate_field(xpath, locator) xpath end # Match any `select` element. # # @param [String] locator # Label, id, or name of the field to match # def select(locator) locator = locator.to_s locate_field(descendant(:select), locator) end # Match any `input` element of type `checkbox`. # # @param [String] locator # Label, id, or name of the checkbox to match # def checkbox(locator) locator = locator.to_s locate_field(descendant(:input)[attr(:type).equals('checkbox')], locator) end # Match any `input` element of type `radio`. # # @param [String] locator # Label, id, or name of the radio button to match # def radio_button(locator) locator = locator.to_s locate_field(descendant(:input)[attr(:type).equals('radio')], locator) end # Match any `input` element of type `file`. # # @param [String] locator # Label, id, or name of the file field to match # def file_field(locator) locator = locator.to_s locate_field(descendant(:input)[attr(:type).equals('file')], locator) end # Match an `optgroup` element. # # @param [String] name # Label for the option group # def optgroup(locator) locator = locator.to_s descendant(:optgroup)[attr(:label).is(locator)] end # Match an `option` element. # # @param [String] name # Visible text of the option # def option(locator) locator = locator.to_s descendant(:option)[string.n.is(locator)] end # Match any `table` element. # # @param [String] locator # Caption or id of the table to match # @option options [Array] :rows # Content of each cell in each row to match # def table(locator) locator = locator.to_s descendant(:table)[attr(:id).equals(locator) | descendant(:caption).is(locator)] end # Match any 'dd' element. # # @param [String] locator # Id of the 'dd' element or text from preciding 'dt' element content def definition_description(locator) locator = locator.to_s descendant(:dd)[attr(:id).equals(locator) | previous_sibling(:dt)[string.n.equals(locator)] ] end protected def locate_field(xpath, locator) locate_field = xpath[attr(:id).equals(locator) | attr(:name).equals(locator) | attr(:placeholder).equals(locator) | attr(:id).equals(anywhere(:label)[string.n.is(locator)].attr(:for))] locate_field += descendant(:label)[string.n.is(locator)].descendant(xpath) locate_field end end end ruby-xpath-2.0.0/lib/xpath/literal.rb000066400000000000000000000001651214726216500174770ustar00rootroot00000000000000module XPath class Literal attr_reader :value def initialize(value) @value = value end end end ruby-xpath-2.0.0/lib/xpath/renderer.rb000066400000000000000000000076051214726216500176570ustar00rootroot00000000000000module XPath class Renderer def self.render(node, type) new(type).render(node) end def initialize(type) @type = type end def render(node) arguments = node.arguments.map { |argument| convert_argument(argument) } send(node.expression, *arguments) end def convert_argument(argument) case argument when Expression, Union then render(argument) when Array then argument.map { |element| convert_argument(element) } when String then string_literal(argument) when Literal then argument.value else argument.to_s end end def string_literal(string) if string.include?("'") string = string.split("'", -1).map do |substr| "'#{substr}'" end.join(%q{,"'",}) "concat(#{string})" else "'#{string}'" end end def this_node '.' end def descendant(parent, element_names) if element_names.length == 1 "#{parent}//#{element_names.first}" elsif element_names.length > 1 "#{parent}//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]" else "#{parent}//*" end end def child(parent, element_names) if element_names.length == 1 "#{parent}/#{element_names.first}" elsif element_names.length > 1 "#{parent}/*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]" else "#{parent}/*" end end def axis(parent, name, tag_name) "#{parent}/#{name}::#{tag_name}" end def node_name(current) "name(#{current})" end def where(on, condition) "#{on}[#{condition}]" end def attribute(current, name) "#{current}/@#{name}" end def equality(one, two) "#{one} = #{two}" end def is(one, two) if @type == :exact equality(one, two) else contains(one, two) end end def variable(name) "%{#{name}}" end def text(current) "#{current}/text()" end def normalized_space(current) "normalize-space(#{current})" end def literal(node) node end def css(current, selector) paths = Nokogiri::CSS.xpath_for(selector).map do |xpath_selector| "#{current}#{xpath_selector}" end union(paths) end def union(*expressions) expressions.join(' | ') end def anywhere(element_names) if element_names.length == 1 "//#{element_names.first}" elsif element_names.length > 1 "//*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]" else "//*" end end def contains(current, value) "contains(#{current}, #{value})" end def starts_with(current, value) "starts-with(#{current}, #{value})" end def and(one, two) "(#{one} and #{two})" end def or(one, two) "(#{one} or #{two})" end def one_of(current, values) values.map { |value| "#{current} = #{value}" }.join(' or ') end def next_sibling(current, element_names) if element_names.length == 1 "#{current}/following-sibling::*[1]/self::#{element_names.first}" elsif element_names.length > 1 "#{current}/following-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]" else "#{current}/following-sibling::*[1]/self::*" end end def previous_sibling(current, element_names) if element_names.length == 1 "#{current}/preceding-sibling::*[1]/self::#{element_names.first}" elsif element_names.length > 1 "#{current}/preceding-sibling::*[1]/self::*[#{element_names.map { |e| "self::#{e}" }.join(" | ")}]" else "#{current}/preceding-sibling::*[1]/self::*" end end def inverse(current) "not(#{current})" end def string_function(current) "string(#{current})" end end end ruby-xpath-2.0.0/lib/xpath/union.rb000066400000000000000000000007771214726216500172040ustar00rootroot00000000000000module XPath class Union include Enumerable attr_reader :expressions alias_method :arguments, :expressions def initialize(*expressions) @expressions = expressions end def expression :union end def each(&block) arguments.each(&block) end def method_missing(*args) XPath::Union.new(*arguments.map { |e| e.send(*args) }) end def to_xpath(type=nil) Renderer.render(self, type) end alias_method :to_s, :to_xpath end end ruby-xpath-2.0.0/lib/xpath/version.rb000066400000000000000000000000451214726216500175250ustar00rootroot00000000000000module XPath VERSION = '2.0.0' end ruby-xpath-2.0.0/metadata.gz.sig000066400000000000000000000004001214726216500165170ustar00rootroot00000000000000Qיִ.ChiΑj5hg/Ϋ!7z4 - !ruby/object:Gem::Version version: '1.3' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: '1.3' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' - !ruby/object:Gem::Dependency name: yard requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 0.5.8 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: 0.5.8 - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' description: XPath is a Ruby DSL for generating XPath expressions email: - jonas.nicklas@gmail.com executables: [] extensions: [] extra_rdoc_files: - README.md files: - lib/xpath/dsl.rb - lib/xpath/expression.rb - lib/xpath/html.rb - lib/xpath/literal.rb - lib/xpath/renderer.rb - lib/xpath/union.rb - lib/xpath/version.rb - lib/xpath.rb - spec/fixtures/form.html - spec/fixtures/simple.html - spec/fixtures/stuff.html - spec/html_spec.rb - spec/spec_helper.rb - spec/union_spec.rb - spec/xpath_spec.rb - README.md homepage: http://github.com/jnicklas/xpath licenses: [] post_install_message: rdoc_options: - --main - README.md require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ! '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: xpath rubygems_version: 1.8.25 signing_key: specification_version: 3 summary: Generate XPath expressions from Ruby test_files: [] has_rdoc: ruby-xpath-2.0.0/spec/000077500000000000000000000000001214726216500145545ustar00rootroot00000000000000ruby-xpath-2.0.0/spec/fixtures/000077500000000000000000000000001214726216500164255ustar00rootroot00000000000000ruby-xpath-2.0.0/spec/fixtures/form.html000066400000000000000000000305511214726216500202620ustar00rootroot00000000000000

Form

An awesome link With id A cool title Alt link A link at a time A link An image that is beautiful An image Href-ed link Wrong Link My whitespaced link An emphatic link with some children

Some Legend
Span Legend
Long legend yo
Long legend
Outer legend
Inner legend

Tables

FirstSecond
Table with caption
FirstSecond
I have nested whitespace I don't

Fields

With id

With name

With placeholder

With referenced label

With parent label

Disabled

ruby-xpath-2.0.0/spec/fixtures/simple.html000066400000000000000000000015131214726216500206040ustar00rootroot00000000000000

Blah

Bax

Bax

Bax

Bax

Blah

allamas

llama

A lot of whitespace

chimp

elephant

flamingo

ruby-xpath-2.0.0/spec/fixtures/stuff.html000066400000000000000000000041231214726216500204420ustar00rootroot00000000000000

This is a test

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. awesome image

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat Redirect pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia text with whitespace id est laborum.

BackToMyself A link came first A link A link with data-method No Href Blank Href Blank Anchor Anchor Anchor on different page Anchor on same page very fine image fine image

Coffee
black hot drink
Milk
white cold drink
ruby-xpath-2.0.0/spec/html_spec.rb000066400000000000000000000470631214726216500170710ustar00rootroot00000000000000require 'spec_helper' require 'nokogiri' describe XPath::HTML do let(:template) { 'form' } let(:template_path) { File.read(File.expand_path("fixtures/#{template}.html", File.dirname(__FILE__))) } let(:doc) { Nokogiri::HTML(template_path) } def get(*args) all(*args).first end def all(*args) type = example.metadata[:type] doc.xpath(XPath::HTML.send(subject, *args).to_xpath(type)).map { |node| node[:data] } end describe '#link' do subject { :link } it("finds links by id") { get('some-id').should == 'link-id' } it("finds links by content") { get('An awesome link').should == 'link-text' } it("finds links by content regardless of whitespace") { get('My whitespaced link').should == 'link-whitespace' } it("finds links with child tags by content") { get('An emphatic link').should == 'link-children' } it("finds links by the content of their child tags") { get('emphatic').should == 'link-children' } it("finds links by approximate content") { get('awesome').should == 'link-text' } it("finds links by title") { get('My title').should == 'link-title' } it("finds links by approximate title") { get('title').should == 'link-title' } it("finds links by image's alt attribute") { get('Alt link').should == 'link-img' } it("finds links by image's approximate alt attribute") { get('Alt').should == 'link-img' } it("does not find links without href attriutes") { get('Wrong Link').should be_nil } it("casts to string") { get(:'some-id').should == 'link-id' } context "with exact match", :type => :exact do it("finds links by content") { get('An awesome link').should == 'link-text' } it("does not find links by approximate content") { get('awesome').should be_nil } it("finds links by title") { get('My title').should == 'link-title' } it("does not find links by approximate title") { get('title').should be_nil } it("finds links by image's alt attribute") { get('Alt link').should == 'link-img' } it("does not find links by image's approximate alt attribute") { get('Alt').should be_nil } end end describe '#button' do subject { :button } context "with submit type" do it("finds buttons by id") { get('submit-with-id').should == 'id-submit' } it("finds buttons by value") { get('submit-with-value').should == 'value-submit' } it("finds buttons by approximate value") { get('mit-with-val').should == 'value-submit' } it("finds buttons by title") { get('My submit title').should == 'title-submit' } it("finds buttons by approximate title") { get('submit title').should == 'title-submit' } context "with exact match", :type => :exact do it("finds buttons by value") { get('submit-with-value').should == 'value-submit' } it("does not find buttons by approximate value") { get('mit-with-val').should be_nil } it("finds buttons by title") { get('My submit title').should == 'title-submit' } it("does not find buttons by approximate title") { get('submit title').should be_nil } end end context "with reset type" do it("finds buttons by id") { get('reset-with-id').should == 'id-reset' } it("finds buttons by value") { get('reset-with-value').should == 'value-reset' } it("finds buttons by approximate value") { get('set-with-val').should == 'value-reset' } it("finds buttons by title") { get('My reset title').should == 'title-reset' } it("finds buttons by approximate title") { get('reset title').should == 'title-reset' } context "with exact match", :type => :exact do it("finds buttons by value") { get('reset-with-value').should == 'value-reset' } it("does not find buttons by approximate value") { get('set-with-val').should be_nil } it("finds buttons by title") { get('My reset title').should == 'title-reset' } it("does not find buttons by approximate title") { get('reset title').should be_nil } end end context "with button type" do it("finds buttons by id") { get('button-with-id').should == 'id-button' } it("finds buttons by value") { get('button-with-value').should == 'value-button' } it("finds buttons by approximate value") { get('ton-with-val').should == 'value-button' } it("finds buttons by title") { get('My button title').should == 'title-button' } it("finds buttons by approximate title") { get('button title').should == 'title-button' } context "with exact match", :type => :exact do it("finds buttons by value") { get('button-with-value').should == 'value-button' } it("does not find buttons by approximate value") { get('ton-with-val').should be_nil } it("finds buttons by title") { get('My button title').should == 'title-button' } it("does not find buttons by approximate title") { get('button title').should be_nil } end end context "with image type" do it("finds buttons by id") { get('imgbut-with-id').should == 'id-imgbut' } it("finds buttons by value") { get('imgbut-with-value').should == 'value-imgbut' } it("finds buttons by approximate value") { get('gbut-with-val').should == 'value-imgbut' } it("finds buttons by alt attribute") { get('imgbut-with-alt').should == 'alt-imgbut' } it("finds buttons by approximate alt attribute") { get('mgbut-with-al').should == 'alt-imgbut' } it("finds buttons by title") { get('My imgbut title').should == 'title-imgbut' } it("finds buttons by approximate title") { get('imgbut title').should == 'title-imgbut' } context "with exact match", :type => :exact do it("finds buttons by value") { get('imgbut-with-value').should == 'value-imgbut' } it("does not find buttons by approximate value") { get('gbut-with-val').should be_nil } it("finds buttons by alt attribute") { get('imgbut-with-alt').should == 'alt-imgbut' } it("does not find buttons by approximate alt attribute") { get('mgbut-with-al').should be_nil } it("finds buttons by title") { get('My imgbut title').should == 'title-imgbut' } it("does not find buttons by approximate title") { get('imgbut title').should be_nil } end end context "with button tag" do it("finds buttons by id") { get('btag-with-id').should == 'id-btag' } it("finds buttons by value") { get('btag-with-value').should == 'value-btag' } it("finds buttons by approximate value") { get('tag-with-val').should == 'value-btag' } it("finds buttons by text") { get('btag-with-text').should == 'text-btag' } it("finds buttons by text ignoring whitespace") { get('My whitespaced button').should == 'btag-with-whitespace' } it("finds buttons by approximate text ") { get('tag-with-tex').should == 'text-btag' } it("finds buttons with child tags by text") { get('An emphatic button').should == 'btag-with-children' } it("finds buttons by text of their children") { get('emphatic').should == 'btag-with-children' } it("finds buttons by title") { get('My btag title').should == 'title-btag' } it("finds buttons by approximate title") { get('btag title').should == 'title-btag' } context "with exact match", :type => :exact do it("finds buttons by value") { get('btag-with-value').should == 'value-btag' } it("does not find buttons by approximate value") { get('tag-with-val').should be_nil } it("finds buttons by text") { get('btag-with-text').should == 'text-btag' } it("does not find buttons by approximate text ") { get('tag-with-tex').should be_nil } it("finds buttons by title") { get('My btag title').should == 'title-btag' } it("does not find buttons by approximate title") { get('btag title').should be_nil } end end context "with unkown type" do it("does not find the button") { get('schmoo button').should be_nil } end it("casts to string") { get(:'tag-with-tex').should == 'text-btag' } end describe '#fieldset' do subject { :fieldset } it("finds fieldsets by id") { get('some-fieldset-id').should == 'fieldset-id' } it("finds fieldsets by legend") { get('Some Legend').should == 'fieldset-legend' } it("finds fieldsets by legend child tags") { get('Span Legend').should == 'fieldset-legend-span' } it("accepts approximate legends") { get('Legend').should == 'fieldset-legend' } it("finds nested fieldsets by legend") { get('Inner legend').should == 'fieldset-inner' } it("casts to string") { get(:'Inner legend').should == 'fieldset-inner' } context "with exact match", :type => :exact do it("finds fieldsets by legend") { get('Some Legend').should == 'fieldset-legend' } it("does not find by approximate legends") { get('Legend').should be_nil } end end describe '#field' do subject { :field } context "by id" do it("finds inputs with no type") { get('input-with-id').should == 'input-with-id-data' } it("finds inputs with text type") { get('input-text-with-id').should == 'input-text-with-id-data' } it("finds inputs with password type") { get('input-password-with-id').should == 'input-password-with-id-data' } it("finds inputs with custom type") { get('input-custom-with-id').should == 'input-custom-with-id-data' } it("finds textareas") { get('textarea-with-id').should == 'textarea-with-id-data' } it("finds select boxes") { get('select-with-id').should == 'select-with-id-data' } it("does not find submit buttons") { get('input-submit-with-id').should be_nil } it("does not find image buttons") { get('input-image-with-id').should be_nil } it("does not find hidden fields") { get('input-hidden-with-id').should be_nil } end context "by name" do it("finds inputs with no type") { get('input-with-name').should == 'input-with-name-data' } it("finds inputs with text type") { get('input-text-with-name').should == 'input-text-with-name-data' } it("finds inputs with password type") { get('input-password-with-name').should == 'input-password-with-name-data' } it("finds inputs with custom type") { get('input-custom-with-name').should == 'input-custom-with-name-data' } it("finds textareas") { get('textarea-with-name').should == 'textarea-with-name-data' } it("finds select boxes") { get('select-with-name').should == 'select-with-name-data' } it("does not find submit buttons") { get('input-submit-with-name').should be_nil } it("does not find image buttons") { get('input-image-with-name').should be_nil } it("does not find hidden fields") { get('input-hidden-with-name').should be_nil } end context "by placeholder" do it("finds inputs with no type") { get('input-with-placeholder').should == 'input-with-placeholder-data' } it("finds inputs with text type") { get('input-text-with-placeholder').should == 'input-text-with-placeholder-data' } it("finds inputs with password type") { get('input-password-with-placeholder').should == 'input-password-with-placeholder-data' } it("finds inputs with custom type") { get('input-custom-with-placeholder').should == 'input-custom-with-placeholder-data' } it("finds textareas") { get('textarea-with-placeholder').should == 'textarea-with-placeholder-data' } it("does not find hidden fields") { get('input-hidden-with-placeholder').should be_nil } end context "by referenced label" do it("finds inputs with no type") { get('Input with label').should == 'input-with-label-data' } it("finds inputs with text type") { get('Input text with label').should == 'input-text-with-label-data' } it("finds inputs with password type") { get('Input password with label').should == 'input-password-with-label-data' } it("finds inputs with custom type") { get('Input custom with label').should == 'input-custom-with-label-data' } it("finds textareas") { get('Textarea with label').should == 'textarea-with-label-data' } it("finds select boxes") { get('Select with label').should == 'select-with-label-data' } it("does not find submit buttons") { get('Input submit with label').should be_nil } it("does not find image buttons") { get('Input image with label').should be_nil } it("does not find hidden fields") { get('Input hidden with label').should be_nil } end context "by parent label" do it("finds inputs with no type") { get('Input with parent label').should == 'input-with-parent-label-data' } it("finds inputs with text type") { get('Input text with parent label').should == 'input-text-with-parent-label-data' } it("finds inputs with password type") { get('Input password with parent label').should == 'input-password-with-parent-label-data' } it("finds inputs with custom type") { get('Input custom with parent label').should == 'input-custom-with-parent-label-data' } it("finds textareas") { get('Textarea with parent label').should == 'textarea-with-parent-label-data' } it("finds select boxes") { get('Select with parent label').should == 'select-with-parent-label-data' } it("does not find submit buttons") { get('Input submit with parent label').should be_nil } it("does not find image buttons") { get('Input image with parent label').should be_nil } it("does not find hidden fields") { get('Input hidden with parent label').should be_nil } end it("casts to string") { get(:'select-with-id').should == 'select-with-id-data' } end describe '#fillable_field' do subject{ :fillable_field } context "by parent label" do it("finds inputs with text type") { get('Label text').should == 'id-text' } it("finds inputs where label has problem chars") { get("Label text's got an apostrophe").should == 'id-problem-text' } end end describe '#select' do subject{ :select } it("finds selects by id") { get('select-with-id').should == 'select-with-id-data' } it("finds selects by name") { get('select-with-name').should == 'select-with-name-data' } it("finds selects by label") { get('Select with label').should == 'select-with-label-data' } it("finds selects by parent label") { get('Select with parent label').should == 'select-with-parent-label-data' } it("casts to string") { get(:'Select with parent label').should == 'select-with-parent-label-data' } end describe '#checkbox' do subject{ :checkbox } it("finds checkboxes by id") { get('input-checkbox-with-id').should == 'input-checkbox-with-id-data' } it("finds checkboxes by name") { get('input-checkbox-with-name').should == 'input-checkbox-with-name-data' } it("finds checkboxes by label") { get('Input checkbox with label').should == 'input-checkbox-with-label-data' } it("finds checkboxes by parent label") { get('Input checkbox with parent label').should == 'input-checkbox-with-parent-label-data' } it("casts to string") { get(:'Input checkbox with parent label').should == 'input-checkbox-with-parent-label-data' } end describe '#radio_button' do subject{ :radio_button } it("finds radio buttons by id") { get('input-radio-with-id').should == 'input-radio-with-id-data' } it("finds radio buttons by name") { get('input-radio-with-name').should == 'input-radio-with-name-data' } it("finds radio buttons by label") { get('Input radio with label').should == 'input-radio-with-label-data' } it("finds radio buttons by parent label") { get('Input radio with parent label').should == 'input-radio-with-parent-label-data' } it("casts to string") { get(:'Input radio with parent label').should == 'input-radio-with-parent-label-data' } end describe '#file_field' do subject{ :file_field } it("finds file fields by id") { get('input-file-with-id').should == 'input-file-with-id-data' } it("finds file fields by name") { get('input-file-with-name').should == 'input-file-with-name-data' } it("finds file fields by label") { get('Input file with label').should == 'input-file-with-label-data' } it("finds file fields by parent label") { get('Input file with parent label').should == 'input-file-with-parent-label-data' } it("casts to string") { get(:'Input file with parent label').should == 'input-file-with-parent-label-data' } end describe "#optgroup" do subject { :optgroup } it("finds optgroups by label") { get('Group A').should == 'optgroup-a' } it("finds optgroups by approximate label") { get('oup A').should == 'optgroup-a' } it("casts to string") { get(:'Group A').should == 'optgroup-a' } context "with exact match", :type => :exact do it("finds by label") { get('Group A').should == 'optgroup-a' } it("does not find by approximate label") { get('oup A').should be_nil } end end describe '#option' do subject{ :option } it("finds by text") { get('Option with text').should == 'option-with-text-data' } it("finds by approximate text") { get('Option with').should == 'option-with-text-data' } it("casts to string") { get(:'Option with text').should == 'option-with-text-data' } context "with exact match", :type => :exact do it("finds by text") { get('Option with text').should == 'option-with-text-data' } it("does not find by approximate text") { get('Option with').should be_nil } end end describe "#table" do subject {:table} it("finds by id") { get('table-with-id').should == 'table-with-id-data' } it("finds by caption") { get('Table with caption').should == 'table-with-caption-data' } it("finds by approximate caption") { get('Table with').should == 'table-with-caption-data' } it("casts to string") { get(:'Table with caption').should == 'table-with-caption-data' } context "with exact match", :type => :exact do it("finds by caption") { get('Table with caption').should == 'table-with-caption-data' } it("does not find by approximate caption") { get('Table with').should be_nil } end end describe "#definition_description" do subject {:definition_description} let(:template) {'stuff'} it("find definition description by id") { get('latte').should == "with-id" } it("find definition description by term") { get("Milk").should == "with-dt" } it("casts to string") { get(:"Milk").should == "with-dt" } end end ruby-xpath-2.0.0/spec/spec_helper.rb000066400000000000000000000000201214726216500173620ustar00rootroot00000000000000require 'xpath' ruby-xpath-2.0.0/spec/union_spec.rb000066400000000000000000000042521214726216500172460ustar00rootroot00000000000000require 'spec_helper' describe XPath::Union do let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) } let(:doc) { Nokogiri::HTML(template) } describe '#expressions' do it "should return the expressions" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = XPath::Union.new(@expr1, @expr2) @collection.expressions.should == [@expr1, @expr2] end end describe '#each' do it "should iterate through the expressions" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = XPath::Union.new(@expr1, @expr2) exprs = [] @collection.each { |expr| exprs << expr } exprs.should == [@expr1, @expr2] end end describe '#map' do it "should map the expressions" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = XPath::Union.new(@expr1, @expr2) @collection.map { |expr| expr.expression }.should == [:descendant, :descendant] end end describe '#to_xpath' do it "should create a valid xpath expression" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div).where(x.attr(:id) == 'foo') } @collection = XPath::Union.new(@expr1, @expr2) @results = doc.xpath(@collection.to_xpath) @results[0][:title].should == 'fooDiv' @results[1].text.should == 'Blah' @results[2].text.should == 'Bax' end end describe '#where and others' do it "should be delegated to the individual expressions" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = XPath::Union.new(@expr1, @expr2) @xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath @xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath @results = doc.xpath(@xpath1) @results[0][:title].should == 'fooDiv' @results = doc.xpath(@xpath2) @results[0][:id].should == 'fooDiv' end end end ruby-xpath-2.0.0/spec/xpath_spec.rb000066400000000000000000000276071214726216500172530ustar00rootroot00000000000000require 'spec_helper' require 'nokogiri' class Thingy include XPath def foo_div descendant(:div).where(attr(:id) == 'foo') end end describe XPath do let(:template) { File.read(File.expand_path('fixtures/simple.html', File.dirname(__FILE__))) } let(:doc) { Nokogiri::HTML(template) } def xpath(type=nil, &block) doc.xpath XPath.generate(&block).to_xpath(type) end it "should work as a mixin" do xpath = Thingy.new.foo_div.to_xpath doc.xpath(xpath).first[:title].should == 'fooDiv' end describe '#descendant' do it "should find nodes that are nested below the current node" do @results = xpath { |x| x.descendant(:p) } @results[0].text.should == "Blah" @results[1].text.should == "Bax" end it "should not find nodes outside the context" do @results = xpath do |x| foo_div = x.descendant(:div).where(x.attr(:id) == 'foo') x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title)) end @results[0].should be_nil end it "should find multiple kinds of nodes" do @results = xpath { |x| x.descendant(:p, :ul) } @results[0].text.should == 'Blah' @results[3].text.should == 'A list' end it "should find all nodes when no arguments given" do @results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].descendant } @results[0].text.should == 'Blah' @results[4].text.should == 'A list' end end describe '#child' do it "should find nodes that are nested directly below the current node" do @results = xpath { |x| x.descendant(:div).child(:p) } @results[0].text.should == "Blah" @results[1].text.should == "Bax" end it "should not find nodes that are nested further down below the current node" do @results = xpath { |x| x.child(:p) } @results[0].should be_nil end it "should find multiple kinds of nodes" do @results = xpath { |x| x.descendant(:div).child(:p, :ul) } @results[0].text.should == 'Blah' @results[3].text.should == 'A list' end it "should find all nodes when no arguments given" do @results = xpath { |x| x.descendant[x.attr(:id) == 'foo'].child } @results[0].text.should == 'Blah' @results[3].text.should == 'A list' end end describe '#axis' do it "should find nodes given the xpath axis" do @results = xpath { |x| x.axis(:descendant, :p) } @results[0].text.should == "Blah" end it "should find nodes given the xpath axis without a specific tag" do @results = xpath { |x| x.descendant(:div)[x.attr(:id) == 'foo'].axis(:descendant) } @results[0][:id].should == "fooDiv" end end describe '#next_sibling' do it "should find nodes which are immediate siblings of the current node" do xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:p) }.first.text.should == 'Bax' xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :p) }.first.text.should == 'Bax' xpath { |x| x.descendant(:p)[x.attr(:title) == 'monkey'].next_sibling(:ul, :p) }.first.text.should == 'A list' xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling(:ul, :li) }.first.should be_nil xpath { |x| x.descendant(:p)[x.attr(:id) == 'fooDiv'].next_sibling }.first.text.should == 'Bax' end end describe '#previous_sibling' do it "should find nodes which are exactly preceding the current node" do xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:p) }.first.text.should == 'Bax' xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :p) }.first.text.should == 'Bax' xpath { |x| x.descendant(:p)[x.attr(:title) == 'gorilla'].previous_sibling(:ul, :p) }.first.text.should == 'A list' xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling(:ul, :li) }.first.should be_nil xpath { |x| x.descendant(:p)[x.attr(:id) == 'wooDiv'].previous_sibling }.first.text.should == 'Bax' end end describe '#anywhere' do it "should find nodes regardless of the context" do @results = xpath do |x| foo_div = x.anywhere(:div).where(x.attr(:id) == 'foo') x.descendant(:p).where(x.attr(:id) == foo_div.attr(:title)) end @results[0].text.should == "Blah" end it "should find multiple kinds of nodes regardless of the context" do @results = xpath do |x| context=x.descendant(:div).where(x.attr(:id)=='woo') context.anywhere(:p, :ul) end @results[0].text.should == 'Blah' @results[3].text.should == 'A list' @results[4].text.should == 'A list' @results[6].text.should == 'Bax' end it "should find all nodes when no arguments given regardless of the context" do @results = xpath do |x| context=x.descendant(:div).where(x.attr(:id)=='woo') context.anywhere end @results[0].name.should == 'html' @results[1].name.should == 'head' @results[2].name.should == 'body' @results[6].text.should == 'Blah' @results[10].text.should == 'A list' @results[13].text.should == 'A list' @results[15].text.should == 'Bax' end end describe '#contains' do it "should find nodes that contain the given string" do @results = xpath do |x| x.descendant(:div).where(x.attr(:title).contains('ooD')) end @results[0][:id].should == "foo" end it "should find nodes that contain the given expression" do @results = xpath do |x| expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id) x.descendant(:div).where(x.attr(:title).contains(expression)) end @results[0][:id].should == "foo" end end describe '#starts_with' do it "should find nodes that begin with the given string" do @results = xpath do |x| x.descendant(:*).where(x.attr(:id).starts_with('foo')) end @results.size.should == 2 @results[0][:id].should == "foo" @results[1][:id].should == "fooDiv" end it "should find nodes that contain the given expression" do @results = xpath do |x| expression = x.anywhere(:div).where(x.attr(:title) == 'fooDiv').attr(:id) x.descendant(:div).where(x.attr(:title).starts_with(expression)) end @results[0][:id].should == "foo" end end describe '#text' do it "should select a node's text" do @results = xpath { |x| x.descendant(:p).where(x.text == 'Bax') } @results[0].text.should == 'Bax' @results[1][:title].should == 'monkey' @results = xpath { |x| x.descendant(:div).where(x.descendant(:p).text == 'Bax') } @results[0][:title].should == 'fooDiv' end end describe '#where' do it "should limit the expression to find only certain nodes" do xpath { |x| x.descendant(:div).where(:"@id = 'foo'") }.first[:title].should == "fooDiv" end it "should be aliased as []" do xpath { |x| x.descendant(:div)[:"@id = 'foo'"] }.first[:title].should == "fooDiv" end end describe '#inverse' do it "should invert the expression" do xpath { |x| x.descendant(:p).where(x.attr(:id).equals('fooDiv').inverse) }.first.text.should == 'Bax' end it "should be aliased as the unary tilde" do xpath { |x| x.descendant(:p).where(~x.attr(:id).equals('fooDiv')) }.first.text.should == 'Bax' end end describe '#equals' do it "should limit the expression to find only certain nodes" do xpath { |x| x.descendant(:div).where(x.attr(:id).equals('foo')) }.first[:title].should == "fooDiv" end it "should be aliased as ==" do xpath { |x| x.descendant(:div).where(x.attr(:id) == 'foo') }.first[:title].should == "fooDiv" end end describe '#is' do it "uses equality when :exact given" do xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should == "fooDiv" xpath(:exact) { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first.should be_nil end it "uses substring matching otherwise" do xpath { |x| x.descendant(:div).where(x.attr(:id).is('foo')) }.first[:title].should == "fooDiv" xpath { |x| x.descendant(:div).where(x.attr(:id).is('oo')) }.first[:title].should == "fooDiv" end end describe '#one_of' do it "should return all nodes where the condition matches" do @results = xpath do |x| p = x.anywhere(:div).where(x.attr(:id) == 'foo').attr(:title) x.descendant(:*).where(x.attr(:id).one_of('foo', p, 'baz')) end @results[0][:title].should == "fooDiv" @results[1].text.should == "Blah" @results[2][:title].should == "bazDiv" end end describe '#and' do it "should find all nodes in both expression" do @results = xpath do |x| x.descendant(:*).where(x.contains('Bax').and(x.attr(:title).equals('monkey'))) end @results[0][:title].should == "monkey" end it "should be aliased as ampersand (&)" do @results = xpath do |x| x.descendant(:*).where(x.contains('Bax') & x.attr(:title).equals('monkey')) end @results[0][:title].should == "monkey" end end describe '#or' do it "should find all nodes in either expression" do @results = xpath do |x| x.descendant(:*).where(x.attr(:id).equals('foo').or(x.attr(:id).equals('fooDiv'))) end @results[0][:title].should == "fooDiv" @results[1].text.should == "Blah" end it "should be aliased as pipe (|)" do @results = xpath do |x| x.descendant(:*).where(x.attr(:id).equals('foo') | x.attr(:id).equals('fooDiv')) end @results[0][:title].should == "fooDiv" @results[1].text.should == "Blah" end end describe '#attr' do it "should be an attribute" do @results = xpath { |x| x.descendant(:div).where(x.attr(:id)) } @results[0][:title].should == "barDiv" @results[1][:title].should == "fooDiv" end end describe '#css' do it "should find nodes by the given CSS selector" do @results = xpath { |x| x.css('#preference p') } @results[0].text.should == 'allamas' @results[1].text.should == 'llama' end it "should respect previous expression" do @results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('p') } @results[0].text.should == 'chimp' @results[1].text.should == 'flamingo' end it "should be composable" do @results = xpath { |x| x.css('#moar').descendant(:p) } @results[0].text.should == 'chimp' @results[1].text.should == 'flamingo' end it "should allow comma separated selectors" do @results = xpath { |x| x.descendant[x.attr(:id) == 'moar'].css('div, p') } @results[0].text.should == 'chimp' @results[1].text.should == 'elephant' @results[2].text.should == 'flamingo' end end describe '#name' do it "should match the node's name" do xpath { |x| x.descendant(:*).where(x.name == 'ul') }.first.text.should == "A list" end end describe '#union' do it "should create a union expression" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = @expr1.union(@expr2) @xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath @xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath @results = doc.xpath(@xpath1) @results[0][:title].should == 'fooDiv' @results = doc.xpath(@xpath2) @results[0][:id].should == 'fooDiv' end it "should be aliased as +" do @expr1 = XPath.generate { |x| x.descendant(:p) } @expr2 = XPath.generate { |x| x.descendant(:div) } @collection = @expr1 + @expr2 @xpath1 = @collection.where(XPath.attr(:id) == 'foo').to_xpath @xpath2 = @collection.where(XPath.attr(:id) == 'fooDiv').to_xpath @results = doc.xpath(@xpath1) @results[0][:title].should == 'fooDiv' @results = doc.xpath(@xpath2) @results[0][:id].should == 'fooDiv' end end end