Merge branch 'xpath'

This commit is contained in:
Jonas Nicklas 2010-08-18 02:43:57 +02:00
commit 8c534e4b70
14 changed files with 62 additions and 402 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ spec.opts
capybara-*.html
.yardoc
doc
.bundle

View File

@ -1,7 +1,14 @@
GIT
remote: git://github.com/jnicklas/xpath.git
revision: 18be9a2
specs:
xpath (0.0.1)
PATH
remote: .
specs:
capybara (0.3.9)
celerity (>= 0.7.9)
culerity (>= 0.2.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@ -17,19 +24,19 @@ GEM
culerity (0.2.10)
ffi (0.6.3)
rake (>= 0.8.7)
json_pure (1.4.3)
json_pure (1.4.6)
launchy (0.3.7)
configuration (>= 0.0.5)
rake (>= 0.8.1)
mime-types (1.16)
nokogiri (1.4.2)
nokogiri (1.4.3.1)
rack (1.2.1)
rack-test (0.5.4)
rack (>= 1.0)
rake (0.8.7)
rspec (1.3.0)
rubyzip (0.9.4)
selenium-webdriver (0.0.26)
selenium-webdriver (0.0.27)
ffi (>= 0.6.1)
json_pure
rubyzip
@ -52,4 +59,5 @@ DEPENDENCIES
rspec (>= 1.2.9)
selenium-webdriver (>= 0.0.3)
sinatra (>= 0.9.4)
xpath!
yard (>= 0.5.8)

View File

@ -29,6 +29,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency("selenium-webdriver", [">= 0.0.3"])
s.add_runtime_dependency("rack", [">= 1.0.0"])
s.add_runtime_dependency("rack-test", [">= 0.5.4"])
s.add_runtime_dependency("xpath", [">= 0.1.0"])
s.add_development_dependency("sinatra", [">= 0.9.4"])
s.add_development_dependency("rspec", [">= 1.2.9"])

View File

@ -1,11 +1,11 @@
require 'timeout'
require 'nokogiri'
require 'xpath'
module Capybara
class CapybaraError < StandardError; end
class DriverNotFoundError < CapybaraError; end
class ElementNotFound < CapybaraError; end
class OptionNotFound < ElementNotFound; end
class UnselectNotAllowed < CapybaraError; end
class NotSupportedByDriverError < CapybaraError; end
class TimeoutError < CapybaraError; end
@ -49,7 +49,6 @@ module Capybara
autoload :Server, 'capybara/server'
autoload :Session, 'capybara/session'
autoload :XPath, 'capybara/xpath'
autoload :Node, 'capybara/node'
autoload :Document, 'capybara/node'
autoload :Element, 'capybara/node'

View File

@ -36,7 +36,8 @@ class Capybara::Driver::RackTest < Capybara::Driver::Base
def set(value)
if tag_name == 'input' and type == 'radio'
driver.html.xpath("//input[@name=#{Capybara::XPath.escape(self[:name])}]").each { |node| node.remove_attribute("checked") }
other_radios_xpath = XPath.generate { |x| x.anywhere(:input)[x.attr(:name).equals(self[:name])] }.to_s
driver.html.xpath(other_radios_xpath).each { |node| node.remove_attribute("checked") }
native['checked'] = 'checked'
elsif tag_name == 'input' and type == 'checkbox'
if value && !native['checked']

View File

@ -72,6 +72,15 @@ module Capybara
base.native
end
##
#
# @deprecated node is deprecated, please use {Capybara::Element#native} instead
#
def node
warn "DEPRECATED: #node is deprecated, please use #native instead"
native
end
##
#
# @return [String] The text of the element

View File

@ -11,7 +11,7 @@ module Capybara
#
def click_link_or_button(locator)
msg = "no link or button '#{locator}' found"
find(:xpath, XPath.link(locator).button(locator), :message => msg).click
find(:xpath, XPath::HTML.link_or_button(locator), :message => msg).click
end
##
@ -23,7 +23,7 @@ module Capybara
#
def click_link(locator)
msg = "no link with title, id or text '#{locator}' found"
find(:xpath, XPath.link(locator), :message => msg).click
find(:xpath, XPath::HTML.link(locator), :message => msg).click
end
##
@ -34,7 +34,7 @@ module Capybara
#
def click_button(locator)
msg = "no button with value or id or text '#{locator}' found"
find(:xpath, XPath.button(locator), :message => msg).click
find(:xpath, XPath::HTML.button(locator), :message => msg).click
end
##
@ -50,7 +50,7 @@ module Capybara
def fill_in(locator, options={})
msg = "cannot fill in, no text field, text area or password field with id, name, or label '#{locator}' found"
raise "Must pass a hash containing 'with'" if not options.is_a?(Hash) or not options.has_key?(:with)
find(:xpath, XPath.fillable_field(locator), :message => msg).set(options[:with])
find(:xpath, XPath::HTML.fillable_field(locator), :message => msg).set(options[:with])
end
##
@ -64,7 +64,7 @@ module Capybara
#
def choose(locator)
msg = "cannot choose field, no radio button with id, name, or label '#{locator}' found"
find(:xpath, XPath.radio_button(locator), :message => msg).set(true)
find(:xpath, XPath::HTML.radio_button(locator), :message => msg).set(true)
end
##
@ -78,7 +78,7 @@ module Capybara
#
def check(locator)
msg = "cannot check field, no checkbox with id, name, or label '#{locator}' found"
find(:xpath, XPath.checkbox(locator), :message => msg).set(true)
find(:xpath, XPath::HTML.checkbox(locator), :message => msg).set(true)
end
##
@ -92,7 +92,7 @@ module Capybara
#
def uncheck(locator)
msg = "cannot uncheck field, no checkbox with id, name, or label '#{locator}' found"
find(:xpath, XPath.checkbox(locator), :message => msg).set(false)
find(:xpath, XPath::HTML.checkbox(locator), :message => msg).set(false)
end
##
@ -108,8 +108,8 @@ module Capybara
def select(value, options={})
no_select_msg = "cannot select option, no select box with id, name, or label '#{options[:from]}' found"
no_option_msg = "cannot select option, no option with text '#{value}' in select box '#{options[:from]}'"
select = find(:xpath, XPath.select(options[:from]), :message => no_select_msg)
select.find(:xpath, XPath.option(value), :message => no_option_msg).select_option
select = find(:xpath, XPath::HTML.select(options[:from]), :message => no_select_msg)
select.find(:xpath, XPath::HTML.option(value), :message => no_option_msg).select_option
end
##
@ -125,8 +125,8 @@ module Capybara
def unselect(value, options={})
no_select_msg = "cannot unselect option, no select box with id, name, or label '#{options[:from]}' found"
no_option_msg = "cannot unselect option, no option with text '#{value}' in select box '#{options[:from]}'"
select = find(:xpath, XPath.select(options[:from]), :message => no_select_msg)
select.find(:xpath, XPath.option(value), :message => no_option_msg).unselect_option
select = find(:xpath, XPath::HTML.select(options[:from]), :message => no_select_msg)
select.find(:xpath, XPath::HTML.option(value), :message => no_option_msg).unselect_option
end
##
@ -141,7 +141,7 @@ module Capybara
#
def attach_file(locator, path)
msg = "cannot attach file, no file field with id, name, or label '#{locator}' found"
find(:xpath, XPath.file_field(locator), :message => msg).set(path)
find(:xpath, XPath::HTML.file_field(locator), :message => msg).set(path)
end
##

View File

@ -47,7 +47,7 @@ module Capybara
# @return [Capybara::Element] The found element
#
def find_field(locator)
find(:xpath, XPath.field(locator))
find(:xpath, XPath::HTML.field(locator))
end
alias_method :field_labeled, :find_field
@ -59,7 +59,7 @@ module Capybara
# @return [Capybara::Element] The found element
#
def find_link(locator)
find(:xpath, XPath.link(locator))
find(:xpath, XPath::HTML.link(locator))
end
##
@ -70,7 +70,7 @@ module Capybara
# @return [Capybara::Element] The found element
#
def find_button(locator)
find(:xpath, XPath.button(locator))
find(:xpath, XPath::HTML.button(locator))
end
##
@ -121,7 +121,7 @@ module Capybara
def all(*args)
options = if args.last.is_a?(Hash) then args.pop else {} end
results = XPath.wrap(normalize_locator(*args)).paths.map do |path|
results = XPath::HTML.wrap(normalize_locator(*args)).map do |path|
base.find(path)
end.flatten
@ -142,7 +142,7 @@ module Capybara
def normalize_locator(kind, locator=nil)
kind, locator = Capybara.default_selector, kind if locator.nil?
locator = XPath.from_css(locator) if kind == :css
locator = XPath::HTML.from_css(locator) if kind == :css
locator
end

View File

@ -30,67 +30,67 @@ module Capybara
end
def has_css?(path, options={})
has_xpath?(XPath.from_css(path), options)
has_xpath?(XPath::HTML.from_css(path), options)
end
def has_no_css?(path, options={})
has_no_xpath?(XPath.from_css(path), options)
has_no_xpath?(XPath::HTML.from_css(path), options)
end
def has_content?(content)
has_xpath?(XPath.content(content))
has_xpath?(XPath::HTML.content(content))
end
def has_no_content?(content)
has_no_xpath?(XPath.content(content))
has_no_xpath?(XPath::HTML.content(content))
end
def has_link?(locator)
has_xpath?(XPath.link(locator))
has_xpath?(XPath::HTML.link(locator))
end
def has_no_link?(locator)
has_no_xpath?(XPath.link(locator))
has_no_xpath?(XPath::HTML.link(locator))
end
def has_button?(locator)
has_xpath?(XPath.button(locator))
has_xpath?(XPath::HTML.button(locator))
end
def has_no_button?(locator)
has_no_xpath?(XPath.button(locator))
has_no_xpath?(XPath::HTML.button(locator))
end
def has_field?(locator, options={})
has_xpath?(XPath.field(locator, options))
has_xpath?(XPath::HTML.field(locator, options))
end
def has_no_field?(locator, options={})
has_no_xpath?(XPath.field(locator, options))
has_no_xpath?(XPath::HTML.field(locator, options))
end
def has_checked_field?(locator)
has_xpath?(XPath.field(locator, :checked => true))
has_xpath?(XPath::HTML.field(locator, :checked => true))
end
def has_unchecked_field?(locator)
has_xpath?(XPath.field(locator, :unchecked => true))
has_xpath?(XPath::HTML.field(locator, :unchecked => true))
end
def has_select?(locator, options={})
has_xpath?(XPath.select(locator, options))
has_xpath?(XPath::HTML.select(locator, options))
end
def has_no_select?(locator, options={})
has_no_xpath?(XPath.select(locator, options))
has_no_xpath?(XPath::HTML.select(locator, options))
end
def has_table?(locator, options={})
has_xpath?(XPath.table(locator, options))
has_xpath?(XPath::HTML.table(locator, options))
end
def has_no_table?(locator, options={})
has_no_xpath?(XPath.table(locator, options))
has_no_xpath?(XPath::HTML.table(locator, options))
end
end
end

View File

@ -170,7 +170,7 @@ module Capybara
# @param [String] locator Id or legend of the fieldset
#
def within_fieldset(locator)
within :xpath, XPath.fieldset(locator) do
within :xpath, XPath::HTML.fieldset(locator) do
yield
end
end
@ -182,7 +182,7 @@ module Capybara
# @param [String] locator Id or caption of the table
#
def within_table(locator)
within :xpath, XPath.table(locator) do
within :xpath, XPath::HTML.table(locator) do
yield
end
end

View File

@ -16,7 +16,7 @@ shared_examples_for "all" do
it "should accept an XPath instance" do
@session.visit('/form')
@xpath = Capybara::XPath.text_field('Name')
@xpath = XPath::HTML.fillable_field('Name')
@result = @session.all(@xpath).map { |r| r.value }
@result.should include('Smith', 'John', 'John Smith')
end

View File

@ -72,7 +72,7 @@ shared_examples_for "find" do
it "should accept an XPath instance and respect the order of paths" do
@session.visit('/form')
@xpath = Capybara::XPath.text_field('Name')
@xpath = XPath::HTML.fillable_field('Name')
@session.find(@xpath).value.should == 'John Smith'
end

View File

@ -1,186 +0,0 @@
module Capybara
##
#
# This is a class for generating XPath queries, use it like this:
#
# Xpath.text_field('foo').link('blah').to_s
#
# This will generate an XPath that matches either a text field or a link.
#
class XPath
class << self
def escape(string)
if string.include?("'")
string = string.split("'", -1).map do |substr|
"'#{substr}'"
end.join(%q{,"'",})
"concat(#{string})"
else
"'#{string}'"
end
end
def wrap(path)
if path.is_a?(self)
path
else
new(path.to_s)
end
end
def respond_to?(method)
new.respond_to?(method)
end
def method_missing(*args)
new.send(*args)
end
end
attr_reader :paths
def initialize(*paths)
@paths = paths
end
def to_s
@paths.join(' | ')
end
def append(path)
XPath.new(*[@paths, XPath.wrap(path).paths].flatten)
end
def prepend(path)
XPath.new(*[XPath.wrap(path).paths, @paths].flatten)
end
def from_css(css)
XPath.new(*[@paths, Nokogiri::CSS.xpath_for(css).map { |selector| '.' + selector }].flatten)
end
alias_method :for_css, :from_css
def field(locator, options={})
if options[:with]
fillable_field(locator, options)
else
xpath = fillable_field(locator)
xpath = xpath.input_field(:file, locator, options)
xpath = xpath.checkbox(locator, options)
xpath = xpath.radio_button(locator, options)
xpath.select(locator, options)
end
end
def fillable_field(locator, options={})
text_area(locator, options).text_field(locator, options)
end
def content(locator)
append("./descendant-or-self::*[contains(normalize-space(.),#{s(locator)})]")
end
def table(locator, options={})
conditions = ""
if options[:rows]
row_conditions = options[:rows].map do |row|
row = row.map { |column| "*[self::td or self::th][text()=#{s(column)}]" }.join(sibling)
"tr[./#{row}]"
end.join(sibling)
conditions << "[.//#{row_conditions}]"
end
append(".//table[@id=#{s(locator)} or contains(caption,#{s(locator)})]#{conditions}")
end
def fieldset(locator)
append(".//fieldset[@id=#{s(locator)} or contains(legend,#{s(locator)})]")
end
def link(locator)
xpath = append(".//a[@href][@id=#{s(locator)} or contains(.,#{s(locator)}) or contains(@title,#{s(locator)}) or img[contains(@alt,#{s(locator)})]]")
xpath.prepend(".//a[@href][text()=#{s(locator)} or @title=#{s(locator)} or img[@alt=#{s(locator)}]]")
end
def button(locator)
xpath = append(".//input[@type='submit' or @type='image' or @type='button'][@id=#{s(locator)} or contains(@value,#{s(locator)})]")
xpath = xpath.append(".//button[@id=#{s(locator)} or contains(@value,#{s(locator)}) or contains(.,#{s(locator)})]")
xpath = xpath.prepend(".//input[@type='submit' or @type='image' or @type='button'][@value=#{s(locator)}]")
xpath = xpath.prepend(".//input[@type='image'][@alt=#{s(locator)} or contains(@alt,#{s(locator)})]")
xpath = xpath.prepend(".//button[@value=#{s(locator)} or text()=#{s(locator)}]")
end
def text_field(locator, options={})
options = options.merge(:value => options[:with]) if options.has_key?(:with)
add_field(locator, ".//input[not(@type) or (@type!='radio' and @type!='checkbox' and @type!='hidden')]", options)
end
def text_area(locator, options={})
options = options.merge(:text => options[:with]) if options.has_key?(:with)
add_field(locator, ".//textarea", options)
end
def select(locator, options={})
add_field(locator, ".//select", options)
end
def checkbox(locator, options={})
input_field(:checkbox, locator, options)
end
def radio_button(locator, options={})
input_field(:radio, locator, options)
end
def file_field(locator, options={})
input_field(:file, locator, options)
end
def option(name)
append(".//option[normalize-space(text())=#{s(name)}]").append(".//option[contains(.,#{s(name)})]")
end
protected
def input_field(type, locator, options={})
options = options.merge(:value => options[:with]) if options.has_key?(:with)
add_field(locator, ".//input[@type='#{type}']", options)
end
# place this between to nodes to indicate that they should be siblings
def sibling
'/following-sibling::*[1]/self::'
end
def add_field(locator, field, options={})
postfix = extract_postfix(options)
xpath = append("#{field}[@id=#{s(locator)}]#{postfix}")
xpath = xpath.append("#{field}[@name=#{s(locator)}]#{postfix}")
xpath = xpath.append("#{field}[@id=//label[contains(.,#{s(locator)})]/@for]#{postfix}")
# FIXME: Label should not be scoped to node, temporary workaround!!!
xpath = xpath.append(".//label[contains(.,#{s(locator)})]/#{field}#{postfix}")
xpath.prepend("#{field}[@id=//label[text()=#{s(locator)}]/@for]#{postfix}")
end
def extract_postfix(options)
options.inject("") do |postfix, (key, value)|
case key
when :value then postfix += "[@value=#{s(value)}]"
when :text then postfix += "[text()=#{s(value)}]"
when :checked then postfix += "[@checked]"
when :unchecked then postfix += "[not(@checked)]"
when :options then postfix += value.map { |o| "[.//option/text()=#{s(o)}]" }.join
when :selected then postfix += [value].flatten.map { |o| "[.//option[@selected]/text()=#{s(o)}]" }.join
end
postfix
end
end
# Sanitize a String for putting it into an xpath query
def s(string)
XPath.escape(string)
end
end
end

View File

@ -1,173 +0,0 @@
require File.expand_path('spec_helper', File.dirname(__FILE__))
describe Capybara::XPath do
before do
@driver = Capybara::Driver::RackTest.new(TestApp)
@driver.visit('/form')
@xpath = Capybara::XPath.new
end
it "should proxy any class method calls to a new instance" do
@query = Capybara::XPath.fillable_field('First Name').to_s
@driver.find(@query).first.value.should == 'John'
end
it "should respond to instance methods at the class level" do
Capybara::XPath.should respond_to(:fillable_field)
end
describe '.wrap' do
it "should return an XPath unmodified" do
Capybara::XPath.wrap(@xpath).should == @xpath
end
it "should wrap a string in an xpath object" do
@xpath = Capybara::XPath.wrap('//foo/bar')
@xpath.should be_an_instance_of(Capybara::XPath)
@xpath.paths.should == ['//foo/bar']
end
end
describe '#append' do
it "should append an XPath's paths" do
@xpath = Capybara::XPath.wrap('//test')
@xpath = @xpath.append(Capybara::XPath.wrap('//foo/bar'))
@xpath.paths.should == ['//test', '//foo/bar']
end
it "should append an String as a new path" do
@xpath = Capybara::XPath.wrap('//test')
@xpath = @xpath.append('//foo/bar')
@xpath.paths.should == ['//test', '//foo/bar']
end
end
describe '#prepend' do
it "should prepend an XPath's paths" do
@xpath = Capybara::XPath.wrap('//test')
@xpath = @xpath.prepend(Capybara::XPath.wrap('//foo/bar'))
@xpath.paths.should == ['//foo/bar', '//test']
end
it "should prepend an String as a new path" do
@xpath = Capybara::XPath.wrap('//test')
@xpath = @xpath.prepend('//foo/bar')
@xpath.paths.should == ['//foo/bar', '//test']
end
end
describe '#field' do
it "should find any field by id or label" do
@query = @xpath.field('First Name').to_s
@driver.find(@query).first.value.should == 'John'
@query = @xpath.field('Password').to_s
@driver.find(@query).first.value.should == 'seeekrit'
@query = @xpath.field('Description').to_s
@driver.find(@query).first.text.should == 'Descriptive text goes here'
@query = @xpath.field('Document').to_s
@driver.find(@query).first[:name].should == 'form[document]'
@query = @xpath.field('Cat').to_s
@driver.find(@query).first.value.should == 'cat'
@query = @xpath.field('Male').to_s
@driver.find(@query).first.value.should == 'male'
@query = @xpath.field('Region').to_s
@driver.find(@query).first[:name].should == 'form[region]'
end
it "should be chainable" do
@query = @xpath.field('First Name').button('Click me!').to_s
@driver.find(@query).first.value.should == 'John'
end
end
describe '#fillable_field' do
it "should find a text field, password field, or text area by id or label" do
@query = @xpath.fillable_field('First Name').to_s
@driver.find(@query).first.value.should == 'John'
@query = @xpath.fillable_field('Password').to_s
@driver.find(@query).first.value.should == 'seeekrit'
@query = @xpath.fillable_field('Description').to_s
@driver.find(@query).first.text.should == 'Descriptive text goes here'
end
it "should be chainable" do
@query = @xpath.fillable_field('First Name').button('Click me!').to_s
@driver.find(@query).first.value.should == 'John'
end
end
describe '#text_area' do
it "should find a text area by id or label" do
@query = @xpath.text_area('form_description').to_s
@driver.find(@query).first.text.should == 'Descriptive text goes here'
@query = @xpath.text_area('Description').to_s
@driver.find(@query).first.text.should == 'Descriptive text goes here'
end
it "should be chainable" do
@query = @xpath.text_area('Description').button('Click me!').to_s
@driver.find(@query).first.text.should == 'Descriptive text goes here'
end
end
describe '#button' do
it "should find a button by id or content" do
@query = @xpath.button('awe123').to_s
@driver.find(@query).first.value.should == 'awesome'
@query = @xpath.button('okay556').to_s
@driver.find(@query).first.value.should == 'okay'
@query = @xpath.button('click_me_123').to_s
@driver.find(@query).first.value.should == 'click_me'
@query = @xpath.button('Click me!').to_s
@driver.find(@query).first.value.should == 'click_me'
@query = @xpath.button('fresh_btn').to_s
@driver.find(@query).first.value.should == 'i am fresh'
@query = @xpath.button('i am fresh').to_s
@driver.find(@query).first[:name].should == 'form[fresh]'
end
end
describe '#radio_button' do
it "should find a radio button by id or label" do
@query = @xpath.radio_button('Male').to_s
@driver.find(@query).first.value.should == 'male'
@query = @xpath.radio_button('gender_male').to_s
@driver.find(@query).first.value.should == 'male'
end
it "should be chainable" do
@query = @xpath.radio_button('Male').button('Click me!').to_s
@driver.find(@query).first.value.should == 'male'
end
end
describe '#checkbox' do
it "should find a checkbox by id or label" do
@query = @xpath.checkbox('Cat').to_s
@driver.find(@query).first.value.should == 'cat'
@query = @xpath.checkbox('form_pets_cat').to_s
@driver.find(@query).first.value.should == 'cat'
end
it "should be chainable" do
@query = @xpath.checkbox('Cat').button('Click me!').to_s
@driver.find(@query).first.value.should == 'cat'
end
end
describe '#select' do
it "should find a select by id or label" do
@query = @xpath.select('Region').to_s
@driver.find(@query).first[:name].should == 'form[region]'
@query = @xpath.select('form_region').to_s
@driver.find(@query).first[:name].should == 'form[region]'
end
it "should be chainable" do
@query = @xpath.select('Region').button('Click me!').to_s
@driver.find(@query).first[:name].should == 'form[region]'
end
end
end