diff --git a/Rakefile b/Rakefile index 8efc2cd3..331eaaf2 100644 --- a/Rakefile +++ b/Rakefile @@ -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'] diff --git a/lib/capybara.rb b/lib/capybara.rb index d26f9730..838a851d 100644 --- a/lib/capybara.rb +++ b/lib/capybara.rb @@ -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' diff --git a/lib/capybara/driver/rack_test_driver.rb b/lib/capybara/driver/rack_test_driver.rb index dca354d5..ceb1f06c 100644 --- a/lib/capybara/driver/rack_test_driver.rb +++ b/lib/capybara/driver/rack_test_driver.rb @@ -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 diff --git a/lib/capybara/node.rb b/lib/capybara/node.rb deleted file mode 100644 index 4fce649c..00000000 --- a/lib/capybara/node.rb +++ /dev/null @@ -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 - %(#) - rescue NotSupportedByDriverError - %(#) - 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 - %(#) - end - end -end diff --git a/lib/capybara/node/actions.rb b/lib/capybara/node/actions.rb index 1d76a103..06d45b0b 100644 --- a/lib/capybara/node/actions.rb +++ b/lib/capybara/node/actions.rb @@ -1,5 +1,5 @@ module Capybara - class Node + module Node module Actions ## diff --git a/lib/capybara/node/base.rb b/lib/capybara/node/base.rb new file mode 100644 index 00000000..798dbec5 --- /dev/null +++ b/lib/capybara/node/base.rb @@ -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 diff --git a/lib/capybara/node/document.rb b/lib/capybara/node/document.rb new file mode 100644 index 00000000..d0feb284 --- /dev/null +++ b/lib/capybara/node/document.rb @@ -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 + %(#) + end + end + end +end diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb new file mode 100644 index 00000000..60f3f099 --- /dev/null +++ b/lib/capybara/node/element.rb @@ -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 + %(#) + rescue NotSupportedByDriverError + %(#) + end + + end + end +end diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb index 99f32a54..794e6a83 100644 --- a/lib/capybara/node/finders.rb +++ b/lib/capybara/node/finders.rb @@ -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 diff --git a/lib/capybara/node/matchers.rb b/lib/capybara/node/matchers.rb index 48b517cc..b3eab6f9 100644 --- a/lib/capybara/node/matchers.rb +++ b/lib/capybara/node/matchers.rb @@ -1,5 +1,5 @@ module Capybara - class Node + module Node module Matchers ## diff --git a/lib/capybara/node/simple.rb b/lib/capybara/node/simple.rb new file mode 100644 index 00000000..a6ac7338 --- /dev/null +++ b/lib/capybara/node/simple.rb @@ -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 diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index 8fe432e8..e0db6e97 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -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) diff --git a/lib/capybara/util/string.rb b/lib/capybara/util/string.rb deleted file mode 100644 index 79962fc7..00000000 --- a/lib/capybara/util/string.rb +++ /dev/null @@ -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 diff --git a/spec/util/string_spec.rb b/spec/basic_node_spec.rb similarity index 100% rename from spec/util/string_spec.rb rename to spec/basic_node_spec.rb diff --git a/spec/string_spec.rb b/spec/string_spec.rb new file mode 100644 index 00000000..ed9e35e5 --- /dev/null +++ b/spec/string_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Capybara do + describe '.string' do + let :string do + Capybara.string <<-STRING +
+
+

Awesome

+

Yes it is

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