Move more of node functionality into subfolder

We have too many top level files, we have multiple
classes in the same file. This allows us to solve
both problems, while also providing a good place
for the new Capybara::Node::Simple (formerly
Capybara::StringNode)
This commit is contained in:
Jonas Nicklas 2010-11-21 14:37:36 +01:00
parent 39f998d846
commit d223d542f7
15 changed files with 393 additions and 300 deletions

View File

@ -3,7 +3,10 @@ require 'rspec/core/rake_task'
require 'yard'
desc "Run all examples"
RSpec::Core::RakeTask.new('spec')
RSpec::Core::RakeTask.new(:spec) do |t|
#t.rspec_path = 'bin/rspec'
t.rspec_opts = %w[--color]
end
YARD::Rake::YardocTask.new do |t|
t.files = ['lib/**/*.rb', 'README.rdoc']

View File

@ -1,7 +1,6 @@
require 'timeout'
require 'nokogiri'
require 'xpath'
module Capybara
class CapybaraError < StandardError; end
class DriverNotFoundError < CapybaraError; end
@ -132,7 +131,7 @@ module Capybara
##
#
# Wraps the given string, which should contain an HTML document or fragment
# in a {Capybara::StringNode} which exposes all {Capybara::Node::Matchers} and
# in a {Capybara::Node::Simple} which exposes all {Capybara::Node::Matchers} and
# {Capybara::Node::Finders}. This allows you to query any string containing
# HTML in the exact same way you would query the current document in a Capybara
# session. For example:
@ -150,10 +149,10 @@ module Capybara
# node.find('ul').find('li').text # => 'Home'
#
# @param [String] html An html fragment or document
# @return [Capybara::StringNode] A node which has Capybara's finders and matchers
# @return [Capybara::Node::Simple] A node which has Capybara's finders and matchers
#
def string(html)
StringNode.new(html)
Capybara::Node::Simple.new(html)
end
##
@ -183,13 +182,19 @@ module Capybara
autoload :Server, 'capybara/server'
autoload :Session, 'capybara/session'
autoload :Node, 'capybara/node'
autoload :StringNode, 'capybara/util/string'
autoload :Document, 'capybara/node'
autoload :Element, 'capybara/node'
autoload :Selector, 'capybara/selector'
autoload :VERSION, 'capybara/version'
module Node
autoload :Base, 'capybara/node/base'
autoload :Simple, 'capybara/node/simple'
autoload :Element, 'capybara/node/element'
autoload :Document, 'capybara/node/document'
autoload :Finders, 'capybara/node/finders'
autoload :Matchers, 'capybara/node/matchers'
autoload :Actions, 'capybara/node/actions'
end
module Driver
autoload :Base, 'capybara/driver/base'
autoload :Node, 'capybara/driver/node'

View File

@ -79,7 +79,7 @@ class Capybara::Driver::RackTest < Capybara::Driver::Base
private
def string_node
@string_node ||= Capybara::StringNode.new(native)
@string_node ||= Capybara::Node::Simple.new(native)
end
# a reference to the select node if this is an option node

View File

@ -1,216 +0,0 @@
require 'capybara/node/finders'
require 'capybara/node/actions'
require 'capybara/node/matchers'
module Capybara
##
#
# A {Capybara::Node} represents either an element on a page through the subclass
# {Capybara::Element} or a document through {Capybara::Document}.
#
# Both types of Node share the same methods, used for interacting with the
# elements on the page. These methods are divided into three categories,
# finders, actions and matchers. These are found in the modules
# {Capybara::Node::Finders}, {Capybara::Node::Actions} and {Capybara::Node::Matchers}
# respectively.
#
# A {Capybara::Session} exposes all methods from {Capybara::Document} directly:
#
# session = Capybara::Session.new(:rack_test, my_app)
# session.visit('/')
# session.fill_in('Foo', :with => 'Bar') # from Capybara::Node::Actions
# bar = session.find('#bar') # from Capybara::Node::Finders
# bar.select('Baz', :from => 'Quox') # from Capybara::Node::Actions
# session.has_css?('#foobar') # from Capybara::Node::Matchers
#
class Node
attr_reader :session, :base
include Capybara::Node::Finders
include Capybara::Node::Actions
include Capybara::Node::Matchers
def initialize(session, base)
@session = session
@base = base
end
protected
def wait?
driver.wait?
end
def driver
session.driver
end
end
##
#
# A {Capybara::Element} represents a single element on the page. It is possible
# to interact with the contents of this element the same as with a document:
#
# session = Capybara::Session.new(:rack_test, my_app)
#
# bar = session.find('#bar') # from Capybara::Node::Finders
# bar.select('Baz', :from => 'Quox') # from Capybara::Node::Actions
#
# {Capybara::Element} also has access to HTML attributes and other properties of the
# element:
#
# bar.value
# bar.text
# bar[:title]
#
# @see Capybara::Node
#
class Element < Node
##
#
# @return [Object] The native element from the driver, this allows access to driver specific methods
#
def native
base.native
end
##
#
# @return [String] The text of the element
#
def text
base.text
end
##
#
# Retrieve the given attribute
#
# element[:title] # => HTML title attribute
#
# @param [Symbol] attribute The attribute to retrieve
# @return [String] The value of the attribute
#
def [](attribute)
base[attribute]
end
##
#
# @return [String] The value of the form element
#
def value
base.value
end
##
#
# Set the value of the form element to the given value.
#
# @param [String] value The new value
#
def set(value)
base.set(value)
end
##
#
# Select this node if is an option element inside a select tag
#
def select_option
base.select_option
end
##
#
# Unselect this node if is an option element inside a multiple select tag
#
def unselect_option
base.unselect_option
end
##
#
# Click the Element
#
def click
base.click
end
##
#
# @return [String] The tag name of the element
#
def tag_name
base.tag_name
end
##
#
# Whether or not the element is visible. Not all drivers support CSS, so
# the result may be inaccurate.
#
# @return [Boolean] Whether the element is visible
#
def visible?
base.visible?
end
##
#
# An XPath expression describing where on the page the element can be found
#
# @return [String] An XPath expression
#
def path
base.path
end
##
#
# Trigger any event on the current element, for example mouseover or focus
# events. Does not work in Selenium.
#
# @param [String] event The name of the event to trigger
#
def trigger(event)
base.trigger(event)
end
##
#
# Drag the element to the given other element.
#
# source = page.find('#foo')
# target = page.find('#bar')
# source.drag_to(target)
#
# @param [Capybara::Element] node The element to drag to
#
def drag_to(node)
base.drag_to(node.base)
end
def inspect
%(#<Capybara::Element tag="#{tag_name}" path="#{path}">)
rescue NotSupportedByDriverError
%(#<Capybara::Element tag="#{tag_name}">)
end
end
##
#
# A {Capybara::Document} represents an HTML document. Any operation
# performed on it will be performed on the entire document.
#
# @see Capybara::Node
#
class Document < Node
def inspect
%(#<Capybara::Document>)
end
end
end

View File

@ -1,5 +1,5 @@
module Capybara
class Node
module Node
module Actions
##

47
lib/capybara/node/base.rb Normal file
View File

@ -0,0 +1,47 @@
module Capybara
module Node
##
#
# A {Capybara::Node::Base} represents either an element on a page through the subclass
# {Capybara::Node::Element} or a document through {Capybara::Node::Document}.
#
# Both types of Node share the same methods, used for interacting with the
# elements on the page. These methods are divided into three categories,
# finders, actions and matchers. These are found in the modules
# {Capybara::Node::Finders}, {Capybara::Node::Actions} and {Capybara::Node::Matchers}
# respectively.
#
# A {Capybara::Session} exposes all methods from {Capybara::Node::Document} directly:
#
# session = Capybara::Session.new(:rack_test, my_app)
# session.visit('/')
# session.fill_in('Foo', :with => 'Bar') # from Capybara::Node::Actions
# bar = session.find('#bar') # from Capybara::Node::Finders
# bar.select('Baz', :from => 'Quox') # from Capybara::Node::Actions
# session.has_css?('#foobar') # from Capybara::Node::Matchers
#
class Base
attr_reader :session, :base
include Capybara::Node::Finders
include Capybara::Node::Actions
include Capybara::Node::Matchers
def initialize(session, base)
@session = session
@base = base
end
protected
def wait?
driver.wait?
end
def driver
session.driver
end
end
end
end

View File

@ -0,0 +1,17 @@
module Capybara
module Node
##
#
# A {Capybara::Document} represents an HTML document. Any operation
# performed on it will be performed on the entire document.
#
# @see Capybara::Node
#
class Document < Base
def inspect
%(#<Capybara::Document>)
end
end
end
end

View File

@ -0,0 +1,158 @@
module Capybara
module Node
##
#
# A {Capybara::Element} represents a single element on the page. It is possible
# to interact with the contents of this element the same as with a document:
#
# session = Capybara::Session.new(:rack_test, my_app)
#
# bar = session.find('#bar') # from Capybara::Node::Finders
# bar.select('Baz', :from => 'Quox') # from Capybara::Node::Actions
#
# {Capybara::Element} also has access to HTML attributes and other properties of the
# element:
#
# bar.value
# bar.text
# bar[:title]
#
# @see Capybara::Node
#
class Element < Base
##
#
# @return [Object] The native element from the driver, this allows access to driver specific methods
#
def native
base.native
end
##
#
# @return [String] The text of the element
#
def text
base.text
end
##
#
# Retrieve the given attribute
#
# element[:title] # => HTML title attribute
#
# @param [Symbol] attribute The attribute to retrieve
# @return [String] The value of the attribute
#
def [](attribute)
base[attribute]
end
##
#
# @return [String] The value of the form element
#
def value
base.value
end
##
#
# Set the value of the form element to the given value.
#
# @param [String] value The new value
#
def set(value)
base.set(value)
end
##
#
# Select this node if is an option element inside a select tag
#
def select_option
base.select_option
end
##
#
# Unselect this node if is an option element inside a multiple select tag
#
def unselect_option
base.unselect_option
end
##
#
# Click the Element
#
def click
base.click
end
##
#
# @return [String] The tag name of the element
#
def tag_name
base.tag_name
end
##
#
# Whether or not the element is visible. Not all drivers support CSS, so
# the result may be inaccurate.
#
# @return [Boolean] Whether the element is visible
#
def visible?
base.visible?
end
##
#
# An XPath expression describing where on the page the element can be found
#
# @return [String] An XPath expression
#
def path
base.path
end
##
#
# Trigger any event on the current element, for example mouseover or focus
# events. Does not work in Selenium.
#
# @param [String] event The name of the event to trigger
#
def trigger(event)
base.trigger(event)
end
##
#
# Drag the element to the given other element.
#
# source = page.find('#foo')
# target = page.find('#bar')
# source.drag_to(target)
#
# @param [Capybara::Element] node The element to drag to
#
def drag_to(node)
base.drag_to(node.base)
end
def inspect
%(#<Capybara::Element tag="#{tag_name}" path="#{path}">)
rescue NotSupportedByDriverError
%(#<Capybara::Element tag="#{tag_name}">)
end
end
end
end

View File

@ -1,5 +1,5 @@
module Capybara
class Node
module Node
module Finders
##
@ -137,7 +137,7 @@ module Capybara
end
def convert_elements(elements)
elements.map { |element| Capybara::Element.new(session, element) }
elements.map { |element| Capybara::Node::Element.new(session, element) }
end
def wait_conditionally_until

View File

@ -1,5 +1,5 @@
module Capybara
class Node
module Node
module Matchers
##

View File

@ -0,0 +1,71 @@
module Capybara
module Node
class Simple
include Capybara::Node::Finders
include Capybara::Node::Matchers
attr_reader :native
def initialize(native)
native = Nokogiri::HTML(native) if native.is_a?(String)
@native = native
end
def text
native.text
end
def [](name)
attr_name = name.to_s
if attr_name == 'value'
value
elsif 'input' == tag_name and 'checkbox' == native[:type] and 'checked' == attr_name
native['checked'] == 'checked'
else
native[attr_name]
end
end
def tag_name
native.node_name
end
def path
native.path
end
def value
if tag_name == 'textarea'
native.content
elsif tag_name == 'select'
if native['multiple'] == 'multiple'
native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
else
option = native.xpath(".//option[@selected='selected']").first || native.xpath(".//option").first
option[:value] || option.content if option
end
else
native[:value]
end
end
def visible?
native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none')]").size == 0
end
protected
def find_in_base(xpath)
native.xpath(xpath).map { |node| self.class.new(node) }
end
def convert_elements(elements)
elements
end
def wait?
false
end
end
end
end

View File

@ -257,7 +257,7 @@ module Capybara
end
def document
Capybara::Document.new(self, driver)
Capybara::Node::Document.new(self, driver)
end
def method_missing(*args)

View File

@ -1,69 +0,0 @@
module Capybara
class StringNode
include Capybara::Node::Finders
include Capybara::Node::Matchers
attr_reader :native
def initialize(native)
native = Nokogiri::HTML(native) if native.is_a?(String)
@native = native
end
def text
native.text
end
def [](name)
attr_name = name.to_s
if attr_name == 'value'
value
elsif 'input' == tag_name and 'checkbox' == native[:type] and 'checked' == attr_name
native['checked'] == 'checked'
else
native[attr_name]
end
end
def tag_name
native.node_name
end
def path
native.path
end
def value
if tag_name == 'textarea'
native.content
elsif tag_name == 'select'
if native['multiple'] == 'multiple'
native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
else
option = native.xpath(".//option[@selected='selected']").first || native.xpath(".//option").first
option[:value] || option.content if option
end
else
native[:value]
end
end
def visible?
native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none')]").size == 0
end
protected
def find_in_base(xpath)
native.xpath(xpath).map { |node| StringNode.new(node) }
end
def convert_elements(elements)
elements
end
def wait?
false
end
end
end

77
spec/string_spec.rb Normal file
View File

@ -0,0 +1,77 @@
require 'spec_helper'
describe Capybara do
describe '.string' do
let :string do
Capybara.string <<-STRING
<div id="page">
<div id="content">
<h1 data="fantastic">Awesome</h1>
<p>Yes it is</p>
</div>
<div id="footer" style="display: none">
<p>c2010</p>
<p>Jonas Nicklas</p>
<input type="text" name="foo" value="bar"/>
<select name="animal">
<option>Monkey</option>
<option selected="selected">Capybara</option>
</select>
</div>
</div>
STRING
end
it "allows using matchers" do
string.should have_css('#page')
string.should_not have_css('#does-not-exist')
end
it "allows using custom matchers" do
Capybara.add_selector :lifeform do
xpath { |name| "//option[contains(.,'#{name}')]" }
end
string.should have_selector(:page)
string.should_not have_selector(:'does-not-exist')
string.should have_selector(:lifeform, "Monkey")
string.should_not have_selector(:lifeform, "Gorilla")
end
it "allows using matchers with text option" do
string.should have_css('h1', :text => 'Awesome')
string.should_not have_css('h1', :text => 'Not so awesome')
end
it "allows finding only visible nodes" do
string.all('//p', :text => 'c2010', :visible => true).should be_empty
string.all('//p', :text => 'c2010', :visible => false).should have(1).element
end
it "allows finding elements and extracting text from them" do
string.find('//h1').text.should == 'Awesome'
end
it "allows finding elements and extracting attributes from them" do
string.find('//h1')[:data].should == 'fantastic'
end
it "allows finding elements and extracting the tag name from them" do
string.find('//h1').tag_name.should == 'h1'
end
it "allows finding elements and extracting the path" do
string.find('//h1').path.should == '/html/body/div/div[1]/h1'
end
it "allows finding elements and extracting the path" do
string.find('//input').value.should == 'bar'
string.find('//select').value.should == 'Capybara'
end
it "allows finding elements and checking if they are visible" do
string.find('//h1').should be_visible
string.find('//input').should_not be_visible
end
end
end