diff --git a/README.rdoc b/README.rdoc index 49c34d78..865c3c45 100644 --- a/README.rdoc +++ b/README.rdoc @@ -153,6 +153,20 @@ For ultimate control, you can instantiate and use a session manually. end session.click_link 'Sign in' +== XPath an CSS + +Capybara does not try to guess what kind of selector you are going to give it, +if you want to use CSS with your 'within' declarations for example, you'll need +to do: + + within(:css, 'ul li') { ... } + +Alternatively you can set the default selector to CSS, which may help if you are +moving from Webrat and used CSS a lot, or simply generally prefer CSS: + + Capybara.default_selector = :css + within('ul li') { ... } + == Gotchas: * Install JRuby and the 'celerity' gem, version 0.7.4 (0.7.5 has a bug with @@ -186,6 +200,14 @@ For ultimate control, you can instantiate and use a session manually. instead. You can achieve this in Rails with link_to('foo', :anchor => '') +== Contributors: + +The following people have dedicated their time and effort to Capybara: + +* Jonas Nicklas +* Dennis Rogenius +* Rob Holland + == License: (The MIT License) diff --git a/Rakefile b/Rakefile index ebfd04f7..7b53f890 100644 --- a/Rakefile +++ b/Rakefile @@ -22,9 +22,8 @@ $hoe = Hoe.spec 'capybara' do ['selenium-webdriver', '>= 0.0.3'], ['rack', '>= 1.0.0'], ['rack-test', '>= 0.5.2'], - ['database_cleaner', '>= 0.2.3'] ] - + self.extra_dev_deps = [ ['sinatra', '>= 0.9.4'], ['rspec', '>= 1.2.9'] diff --git a/lib/capybara/cucumber.rb b/lib/capybara/cucumber.rb index e742fada..679f6055 100644 --- a/lib/capybara/cucumber.rb +++ b/lib/capybara/cucumber.rb @@ -7,10 +7,6 @@ After do Capybara.reset_sessions! end -require 'database_cleaner' -require 'database_cleaner/cucumber' -DatabaseCleaner.strategy = :truncation - Before('@javascript') do Capybara.current_driver = Capybara.javascript_driver end @@ -29,4 +25,4 @@ end After do Capybara.use_default_driver -end \ No newline at end of file +end diff --git a/lib/capybara/dsl.rb b/lib/capybara/dsl.rb index 9054b643..ed0a2348 100644 --- a/lib/capybara/dsl.rb +++ b/lib/capybara/dsl.rb @@ -9,7 +9,7 @@ module Capybara end def current_driver - @current_driver || default_driver + @current_driver || default_driver end alias_method :mode, :current_driver @@ -18,11 +18,17 @@ module Capybara end def use_default_driver - @current_driver = nil + @current_driver = nil end def current_session - session_pool["#{current_driver}#{app.object_id}"] ||= Capybara::Session.new(current_driver, app) + session_pool["#{current_driver}#{app.object_id}"] ||= begin + Capybara::Session.new(current_driver, app) + end + end + + def current_session? + session_pool.has_key?("#{current_driver}#{app.object_id}") end def reset_sessions! diff --git a/lib/capybara/save_and_open_page.rb b/lib/capybara/save_and_open_page.rb index 33c74413..33108cd6 100644 --- a/lib/capybara/save_and_open_page.rb +++ b/lib/capybara/save_and_open_page.rb @@ -3,10 +3,14 @@ module Capybara extend(self) def save_and_open_page(html) - require 'tempfile' - tempfile = Tempfile.new("capybara#{rand(1000000)}") + name="capybara-#{Time.new.strftime("%Y%m%d%H%M%S")}.html" + + FileUtils.touch(name) unless File.exist?(name) + + tempfile = File.new(name,'w') tempfile.write(rewrite_css_and_image_references(html)) tempfile.close + open_in_browser(tempfile.path) end diff --git a/lib/capybara/session.rb b/lib/capybara/session.rb index f8a60e40..20ec4e0a 100644 --- a/lib/capybara/session.rb +++ b/lib/capybara/session.rb @@ -1,190 +1,200 @@ -class Capybara::Session - - FIELDS_PATHS = { - :text_field => proc { |id| "//input[@type='text'][@id='#{id}']" }, - :text_area => proc { |id| "//textarea[@id='#{id}']" }, - :password_field => proc { |id| "//input[@type='password'][@id='#{id}']" }, - :radio => proc { |id| "//input[@type='radio'][@id='#{id}']" }, - :hidden_field => proc { |id| "//input[@type='hidden'][@id='#{id}']" }, - :checkbox => proc { |id| "//input[@type='checkbox'][@id='#{id}']" }, - :select => proc { |id| "//select[@id='#{id}']" }, - :file_field => proc { |id| "//input[@type='file'][@id='#{id}']" } - } - - attr_reader :mode, :app +module Capybara - def initialize(mode, app) - @mode = mode - @app = app - end + class << self + attr_writer :default_selector - def driver - @driver ||= case mode - when :rack_test - Capybara::Driver::RackTest.new(app) - when :culerity - Capybara::Driver::Culerity.new(app) - when :selenium - Capybara::Driver::Selenium.new(app) - else - raise Capybara::DriverNotFoundError, "no driver called #{mode} was found" + def default_selector + @default_selector ||= :xpath end end - def visit(path) - driver.visit(path) - end - - def click_link(locator) - find_link(locator).click - end + class Session - def click_button(locator) - find_button(locator).click - end + FIELDS_PATHS = { + :text_field => proc { |id| "//input[@type='text'][@id='#{id}']" }, + :text_area => proc { |id| "//textarea[@id='#{id}']" }, + :password_field => proc { |id| "//input[@type='password'][@id='#{id}']" }, + :radio => proc { |id| "//input[@type='radio'][@id='#{id}']" }, + :hidden_field => proc { |id| "//input[@type='hidden'][@id='#{id}']" }, + :checkbox => proc { |id| "//input[@type='checkbox'][@id='#{id}']" }, + :select => proc { |id| "//select[@id='#{id}']" }, + :file_field => proc { |id| "//input[@type='file'][@id='#{id}']" } + } - def fill_in(locator, options={}) - find_field(locator, :text_field, :text_area, :password_field).set(options[:with]) - end - - def choose(locator) - find_field(locator, :radio).set(true) - end - - def check(locator) - find_field(locator, :checkbox).set(true) - end - - def uncheck(locator) - find_field(locator, :checkbox).set(false) - end - - def select(value, options={}) - find_field(options[:from], :select).select(value) - end - - def attach_file(locator, path) - find_field(locator, :file_field).set(path) - end + attr_reader :mode, :app - def body - driver.body - end - - def has_content?(content) - has_xpath?("//*[contains(.,'#{content}')]") - end - - def has_xpath?(path, options={}) - results = find(path) - if options[:text] - results = filter_by_text(results, options[:text]) + def initialize(mode, app) + @mode = mode + @app = app end - if options[:count] - results.size == options[:count] - else - results.size > 0 - end - end - - def has_css?(path, options={}) - has_xpath?(css_to_xpath(path), options) - end - - def within(kind, scope=nil) - kind, scope = :xpath, kind unless scope - scope = css_to_xpath(scope) if kind == :css - raise Capybara::ElementNotFound, "scope '#{scope}' not found on page" if find(scope).empty? - scopes.push(scope) - yield - scopes.pop - end - - def within_fieldset(locator) - within "//fieldset[@id='#{locator}' or contains(legend,'#{locator}')]" do - yield - end - end - - def within_table(locator) - within "//table[@id='#{locator}' or contains(caption,'#{locator}')]" do - yield - end - end - - def save_and_open_page - require 'capybara/save_and_open_page' - Capybara::SaveAndOpenPage.save_and_open_page(body) - end - def find_field(locator, *kinds) - kinds = FIELDS_PATHS.keys if kinds.empty? - field = find_field_by_id(locator, *kinds) || find_field_by_label(locator, *kinds) - raise Capybara::ElementNotFound, "no field of kind #{kinds.inspect} with id or label '#{locator}' found" unless field - field - end - alias_method :field_labeled, :find_field - - def find_link(locator) - link = find("//a[@id='#{locator}' or contains(.,'#{locator}') or @title='#{locator}']").first - raise Capybara::ElementNotFound, "no link with title, id or text '#{locator}' found" unless link - link - end - - def find_button(locator) - button = find("//input[@type='submit' or @type='image'][@id='#{locator}' or @value='#{locator}']").first || find("//button[@id='#{locator}' or @value='#{locator}' or contains(.,'#{locator}')]").first - raise Capybara::ElementNotFound, "no button with value or id '#{locator}' found" unless button - button - end - -private - - def css_to_xpath(css) - Nokogiri::CSS.xpath_for(css).first - end - - def filter_by_text(nodes, text) - nodes.select do |node| - case text - when String - node.text.include?(text) - when Regexp - node.text =~ text + def driver + @driver ||= case mode + when :rack_test + Capybara::Driver::RackTest.new(app) + when :culerity + Capybara::Driver::Culerity.new(app) + when :selenium + Capybara::Driver::Selenium.new(app) + else + raise Capybara::DriverNotFoundError, "no driver called #{mode} was found" end end - end - def current_scope - scopes.join('') - end - - def scopes - @scopes ||= [] - end - - def find_field_by_id(locator, *kinds) - kinds.each do |kind| - path = FIELDS_PATHS[kind] - element = find(path.call(locator)).first - return element if element + def visit(path) + driver.visit(path) end - return nil - end - def find_field_by_label(locator, *kinds) - kinds.each do |kind| - label = find("//label[contains(.,'#{locator}')]").first - if label - element = find_field_by_id(label[:for], kind) + def click_link(locator) + find_link(locator).click + end + + def click_button(locator) + find_button(locator).click + end + + def fill_in(locator, options={}) + find_field(locator, :text_field, :text_area, :password_field).set(options[:with]) + end + + def choose(locator) + find_field(locator, :radio).set(true) + end + + def check(locator) + find_field(locator, :checkbox).set(true) + end + + def uncheck(locator) + find_field(locator, :checkbox).set(false) + end + + def select(value, options={}) + find_field(options[:from], :select).select(value) + end + + def attach_file(locator, path) + find_field(locator, :file_field).set(path) + end + + def body + driver.body + end + + def has_content?(content) + has_xpath?("//*[contains(.,'#{content}')]") + end + + def has_xpath?(path, options={}) + results = find(path) + if options[:text] + results = filter_by_text(results, options[:text]) + end + if options[:count] + results.size == options[:count] + else + results.size > 0 + end + end + + def has_css?(path, options={}) + has_xpath?(css_to_xpath(path), options) + end + + def within(kind, scope=nil) + kind, scope = Capybara.default_selector, kind unless scope + scope = css_to_xpath(scope) if kind == :css + raise Capybara::ElementNotFound, "scope '#{scope}' not found on page" if find(scope).empty? + scopes.push(scope) + yield + scopes.pop + end + + def within_fieldset(locator) + within "//fieldset[@id='#{locator}' or contains(legend,'#{locator}')]" do + yield + end + end + + def within_table(locator) + within "//table[@id='#{locator}' or contains(caption,'#{locator}')]" do + yield + end + end + + def save_and_open_page + require 'capybara/save_and_open_page' + Capybara::SaveAndOpenPage.save_and_open_page(body) + end + + def find_field(locator, *kinds) + kinds = FIELDS_PATHS.keys if kinds.empty? + field = find_field_by_id(locator, *kinds) || find_field_by_label(locator, *kinds) + raise Capybara::ElementNotFound, "no field of kind #{kinds.inspect} with id or label '#{locator}' found" unless field + field + end + alias_method :field_labeled, :find_field + + def find_link(locator) + link = find("//a[@id='#{locator}' or contains(.,'#{locator}') or @title='#{locator}']").first + raise Capybara::ElementNotFound, "no link with title, id or text '#{locator}' found" unless link + link + end + + def find_button(locator) + button = find("//input[@type='submit' or @type='image'][@id='#{locator}' or @value='#{locator}']").first || find("//button[@id='#{locator}' or @value='#{locator}' or contains(.,'#{locator}')]").first + raise Capybara::ElementNotFound, "no button with value or id '#{locator}' found" unless button + button + end + + private + + def css_to_xpath(css) + Nokogiri::CSS.xpath_for(css).first + end + + def filter_by_text(nodes, text) + nodes.select do |node| + case text + when String + node.text.include?(text) + when Regexp + node.text =~ text + end + end + end + + def current_scope + scopes.join('') + end + + def scopes + @scopes ||= [] + end + + def find_field_by_id(locator, *kinds) + kinds.each do |kind| + path = FIELDS_PATHS[kind] + element = find(path.call(locator)).first return element if element end + return nil + end + + def find_field_by_label(locator, *kinds) + kinds.each do |kind| + label = find("//label[contains(.,'#{locator}')]").first + if label + element = find_field_by_id(label[:for], kind) + return element if element + end + end + return nil + end + + def find(locator) + locator = current_scope.to_s + locator + driver.find(locator) end - return nil end - - def find(locator) - locator = current_scope.to_s + locator - driver.find(locator) - end - end diff --git a/spec/save_and_open_page_spec.rb b/spec/save_and_open_page_spec.rb new file mode 100644 index 00000000..af26a07b --- /dev/null +++ b/spec/save_and_open_page_spec.rb @@ -0,0 +1,43 @@ +require File.expand_path('spec_helper', File.dirname(__FILE__)) + +require 'capybara/save_and_open_page' +require 'launchy' +describe Capybara::SaveAndOpenPage do + describe "#save_save_and_open_page" do + before do + + @time = Time.new.strftime("%Y%m%d%H%M%S") + @name = "capybara-#{@time}.html" + + @temp_file = mock("FILE") + @temp_file.stub!(:write) + @temp_file.stub!(:close) + @temp_file.stub!(:path).and_return(@name) + + File.should_receive(:exist?).and_return true + File.should_receive(:new).and_return @temp_file + + @html = <<-HTML + + + + +

test

+ + + HTML + + Launchy::Browser.stub(:run) + end + + it "should create a new temporary file" do + @temp_file.should_receive(:write).with @html + Capybara::SaveAndOpenPage.save_and_open_page @html + end + + it "should open the file in the browser" do + Capybara::SaveAndOpenPage.should_receive(:open_in_browser).with(@name) + Capybara::SaveAndOpenPage.save_and_open_page @html + end + end +end diff --git a/spec/session_spec.rb b/spec/session_spec.rb index 5a3f0cd3..436075d1 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -542,7 +542,27 @@ shared_examples_for "session" do @session.body.should include('

Bar

') end end - + + context "with the default selector" do + it "should use XPath" do + @session.within("//li[contains(., 'With Simple HTML')]") do + @session.click_link('Go') + end + @session.body.should include('

Bar

') + end + end + + context "with the default selector set to CSS" do + it "should use CSS" do + Capybara.default_selector = :css + @session.within("ul li[contains('With Simple HTML')]") do + @session.click_link('Go') + end + @session.body.should include('

Bar

') + Capybara.default_selector = :xpath + end + end + context "with click_link" do it "should click links in the given scope" do @session.within("//li[contains(.,'With Simple HTML')]") do