Split up rack-test driver into multiple classes

This commit is contained in:
Jonas Nicklas 2011-04-05 17:42:12 +02:00
parent 97af9bd1c6
commit 8ce6d4eb9d
8 changed files with 413 additions and 325 deletions

View File

@ -199,9 +199,15 @@ module Capybara
module Driver
autoload :Base, 'capybara/driver/base'
autoload :Node, 'capybara/driver/node'
autoload :RackTest, 'capybara/driver/rack_test_driver'
autoload :Selenium, 'capybara/driver/selenium_driver'
end
module RackTest
autoload :Driver, 'capybara/rack_test/driver'
autoload :Node, 'capybara/rack_test/node'
autoload :Form, 'capybara/rack_test/form'
autoload :Browser, 'capybara/rack_test/browser'
end
end
Capybara.configure do |config|
@ -216,7 +222,7 @@ Capybara.configure do |config|
end
Capybara.register_driver :rack_test do |app|
Capybara::Driver::RackTest.new(app)
Capybara::RackTest::Driver.new(app)
end
Capybara.register_driver :selenium do |app|

View File

@ -1,321 +0,0 @@
require 'rack/test'
require 'rack/utils'
require 'mime/types'
require 'nokogiri'
require 'cgi'
class Capybara::Driver::RackTest < Capybara::Driver::Base
class Node < Capybara::Driver::Node
def text
native.text
end
def [](name)
string_node[name]
end
def value
string_node.value
end
def set(value)
if tag_name == 'input' and type == 'radio'
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']
native['checked'] = 'checked'
elsif !value && native['checked']
native.remove_attribute('checked')
end
elsif tag_name == 'input'
if (type == 'text' || type == 'password') && self[:maxlength]
value = value[0...self[:maxlength].to_i]
end
native['value'] = value.to_s
elsif tag_name == "textarea"
native.content = value.to_s
end
end
def select_option
if select_node['multiple'] != 'multiple'
select_node.find(".//option[@selected]").each { |node| node.native.remove_attribute("selected") }
end
native["selected"] = 'selected'
end
def unselect_option
if select_node['multiple'] != 'multiple'
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
end
native.remove_attribute('selected')
end
def click
if tag_name == 'a'
method = self["data-method"] || :get
driver.follow(method, self[:href].to_s)
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
((tag_name == 'button') and type.nil? or type == "submit")
Form.new(driver, form).submit(self)
end
end
def tag_name
native.node_name
end
def visible?
string_node.visible?
end
def checked?
string_node.checked?
end
def selected?
string_node.selected?
end
def path
native.path
end
def find(locator)
native.xpath(locator).map { |n| self.class.new(driver, n) }
end
private
def string_node
@string_node ||= Capybara::Node::Simple.new(native)
end
# a reference to the select node if this is an option node
def select_node
find('./ancestor::select').first
end
def type
native[:type]
end
def form
native.ancestors('form').first
end
end
class Form < Node
# This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for
# the class specifically when determing whether to consturct the request as multipart.
# That check should be based solely on the form element's 'enctype' attribute value,
# which should probably be provided to Rack::Test in its non-GET request methods.
class NilUploadedFile < Rack::Test::UploadedFile
def initialize
@empty_file = Tempfile.new("nil_uploaded_file")
@empty_file.close
end
def original_filename; ""; end
def content_type; "application/octet-stream"; end
def path; @empty_file.path; end
end
def params(button)
params = {}
native.xpath("(.//input|.//select|.//textarea)[not(@disabled)]").map do |field|
case field.name
when 'input'
if %w(radio checkbox).include? field['type']
merge_param!(params, field['name'].to_s, field['value'].to_s) if field['checked']
elsif %w(submit image).include? field['type']
# TO DO identify the click button here (in document order, rather
# than leaving until the end of the params)
elsif field['type'] =='file'
if multipart?
file = \
if (value = field['value']).to_s.empty?
NilUploadedFile.new
else
content_type = MIME::Types.type_for(value).first.to_s
Rack::Test::UploadedFile.new(value, content_type)
end
merge_param!(params, field['name'].to_s, file)
else
merge_param!(params, field['name'].to_s, File.basename(field['value'].to_s))
end
else
merge_param!(params, field['name'].to_s, field['value'].to_s)
end
when 'select'
if field['multiple'] == 'multiple'
options = field.xpath(".//option[@selected]")
options.each do |option|
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s)
end
else
option = field.xpath(".//option[@selected]").first
option ||= field.xpath('.//option').first
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s) if option
end
when 'textarea'
merge_param!(params, field['name'].to_s, field.text.to_s)
end
end
merge_param!(params, button[:name], button[:value] || "") if button[:name]
params
end
def submit(button)
driver.submit(method, native['action'].to_s, params(button))
end
def multipart?
self[:enctype] == "multipart/form-data"
end
private
def method
self[:method] =~ /post/i ? :post : :get
end
def merge_param!(params, key, value)
Rack::Utils.normalize_params(params, key, value)
end
end
include ::Rack::Test::Methods
attr_reader :app
attr_accessor :current_host
alias_method :response, :last_response
alias_method :request, :last_request
def initialize(app)
raise ArgumentError, "rack-test requires a rack application, but none was given" unless app
@app = app
end
def visit(path, attributes = {})
reset_host!
process(:get, path, attributes)
end
def submit(method, path, attributes)
path = request_path if not path or path.empty?
process(method, path, attributes)
end
def follow(method, path, attributes = {})
return if path.gsub(/^#{request_path}/, '').start_with?('#')
process(method, path, attributes)
end
def process(method, path, attributes = {})
new_uri = URI.parse(path)
current_uri = URI.parse(current_url)
path = request_path + path if path.start_with?('?')
path = current_host + path if path.start_with?('/')
if new_uri.host
@current_host = new_uri.scheme + '://' + new_uri.host
end
send(method, to_binary(path), to_binary( attributes ), env)
follow_redirects!
end
def reset_host!
@current_host = (Capybara.app_host || Capybara.default_host)
end
def current_url
request.url rescue ""
end
def response_headers
response.headers
end
def status_code
response.status
end
def to_binary(object)
return object unless Kernel.const_defined?(:Encoding)
if object.respond_to?(:force_encoding)
object.dup.force_encoding(Encoding::ASCII_8BIT)
elsif object.respond_to?(:each_pair) #Hash
{}.tap { |x| object.each_pair {|k,v| x[to_binary(k)] = to_binary(v) } }
elsif object.respond_to?(:each) #Array
object.map{|x| to_binary(x)}
else
object
end
end
def find(selector)
html.xpath(selector).map { |node| Node.new(self, node) }
end
def body
@body ||= response.body
end
def html
@html ||= Nokogiri::HTML(body)
end
alias_method :source, :body
def reset!
reset_host!
clear_rack_mock_session
end
def get(*args, &block); reset_cache; super; end
def post(*args, &block); reset_cache; super; end
def put(*args, &block); reset_cache; super; end
def delete(*args, &block); reset_cache; super; end
def follow_redirects!
5.times do
follow_redirect! if response.redirect?
end
raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if response.redirect?
end
private
def reset_cache
@body = nil
@html = nil
end
# Rack::Test::Methods does not provide methods for manipulating the session
# list so these must be manipulated directly.
def clear_rack_mock_session
@_rack_test_sessions = nil
@_rack_mock_sessions = nil
end
def request_path
request.path rescue ""
end
def env
env = {}
begin
env["HTTP_REFERER"] = request.url
rescue Rack::Test::Error
# no request yet
end
env
end
end

View File

@ -0,0 +1,109 @@
class Capybara::RackTest::Browser
include ::Rack::Test::Methods
attr_reader :app
attr_accessor :current_host
def initialize(app)
@app = app
end
def visit(path, attributes = {})
reset_host!
process(:get, path, attributes)
end
def submit(method, path, attributes)
path = request_path if not path or path.empty?
process(method, path, attributes)
end
def follow(method, path, attributes = {})
return if path.gsub(/^#{request_path}/, '').start_with?('#')
process(method, path, attributes)
end
def follow_redirects!
5.times do
follow_redirect! if last_response.redirect?
end
raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if last_response.redirect?
end
def process(method, path, attributes = {})
new_uri = URI.parse(path)
current_uri = URI.parse(current_url)
path = request_path + path if path.start_with?('?')
path = current_host + path if path.start_with?('/')
if new_uri.host
@current_host = new_uri.scheme + '://' + new_uri.host
end
reset_cache!
send(method, to_binary(path), to_binary( attributes ), env)
follow_redirects!
end
def current_url
last_request.url rescue ""
end
def reset_host!
@current_host = (Capybara.app_host || Capybara.default_host)
end
def reset_cache!
@dom = nil
end
def body
dom.to_xml
end
def dom
@dom ||= Nokogiri::HTML(source)
end
def find(selector)
dom.xpath(selector).map { |node| Capybara::RackTest::Node.new(self, node) }
end
def source
last_response.body
rescue Rack::Test::Error
nil
end
protected
def to_binary(object)
return object unless Kernel.const_defined?(:Encoding)
if object.respond_to?(:force_encoding)
object.dup.force_encoding(Encoding::ASCII_8BIT)
elsif object.respond_to?(:each_pair) #Hash
{}.tap { |x| object.each_pair {|k,v| x[to_binary(k)] = to_binary(v) } }
elsif object.respond_to?(:each) #Array
object.map{|x| to_binary(x)}
else
object
end
end
def request_path
request.path rescue ""
end
def env
env = {}
begin
env["HTTP_REFERER"] = last_request.url
rescue Rack::Test::Error
# no request yet
end
env
end
end

View File

@ -0,0 +1,75 @@
require 'rack/test'
require 'rack/utils'
require 'mime/types'
require 'nokogiri'
require 'cgi'
class Capybara::RackTest::Driver < Capybara::Driver::Base
attr_reader :app
def initialize(app)
raise ArgumentError, "rack-test requires a rack application, but none was given" unless app
@app = app
end
def browser
@browser ||= Capybara::RackTest::Browser.new(app)
end
def response
browser.last_response
end
def request
browser.last_request
end
def visit(path, attributes = {})
browser.visit(path, attributes)
end
def submit(method, path, attributes)
browser.submit(method, path, attributes)
end
def follow(method, path, attributes = {})
browser.follow(method, path, attributes)
end
def current_url
browser.current_url
end
def response_headers
response.headers
end
def status_code
response.status
end
def find(selector)
browser.find(selector)
end
def body
browser.body
end
def source
browser.source
end
def dom
browser.dom
end
def reset!
@browser = nil
end
def get(*args, &block); browser.get(*args, &block); end
def post(*args, &block); browser.post(*args, &block); end
def put(*args, &block); browser.put(*args, &block); end
def delete(*args, &block); browser.delete(*args, &block); end
end

View File

@ -0,0 +1,80 @@
class Capybara::RackTest::Form < Capybara::RackTest::Node
# This only needs to inherit from Rack::Test::UploadedFile because Rack::Test checks for
# the class specifically when determing whether to consturct the request as multipart.
# That check should be based solely on the form element's 'enctype' attribute value,
# which should probably be provided to Rack::Test in its non-GET request methods.
class NilUploadedFile < Rack::Test::UploadedFile
def initialize
@empty_file = Tempfile.new("nil_uploaded_file")
@empty_file.close
end
def original_filename; ""; end
def content_type; "application/octet-stream"; end
def path; @empty_file.path; end
end
def params(button)
params = {}
native.xpath("(.//input|.//select|.//textarea)[not(@disabled)]").map do |field|
case field.name
when 'input'
if %w(radio checkbox).include? field['type']
merge_param!(params, field['name'].to_s, field['value'].to_s) if field['checked']
elsif %w(submit image).include? field['type']
# TO DO identify the click button here (in document order, rather
# than leaving until the end of the params)
elsif field['type'] =='file'
if multipart?
file = \
if (value = field['value']).to_s.empty?
NilUploadedFile.new
else
content_type = MIME::Types.type_for(value).first.to_s
Rack::Test::UploadedFile.new(value, content_type)
end
merge_param!(params, field['name'].to_s, file)
else
merge_param!(params, field['name'].to_s, File.basename(field['value'].to_s))
end
else
merge_param!(params, field['name'].to_s, field['value'].to_s)
end
when 'select'
if field['multiple'] == 'multiple'
options = field.xpath(".//option[@selected]")
options.each do |option|
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s)
end
else
option = field.xpath(".//option[@selected]").first
option ||= field.xpath('.//option').first
merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s) if option
end
when 'textarea'
merge_param!(params, field['name'].to_s, field.text.to_s)
end
end
merge_param!(params, button[:name], button[:value] || "") if button[:name]
params
end
def submit(button)
driver.submit(method, native['action'].to_s, params(button))
end
def multipart?
self[:enctype] == "multipart/form-data"
end
private
def method
self[:method] =~ /post/i ? :post : :get
end
def merge_param!(params, key, value)
Rack::Utils.normalize_params(params, key, value)
end
end

View File

@ -0,0 +1,101 @@
class Capybara::RackTest::Node < Capybara::Driver::Node
def text
native.text
end
def [](name)
string_node[name]
end
def value
string_node.value
end
def set(value)
if tag_name == 'input' and type == 'radio'
other_radios_xpath = XPath.generate { |x| x.anywhere(:input)[x.attr(:name).equals(self[:name])] }.to_s
driver.dom.xpath(other_radios_xpath).each { |node| node.remove_attribute("checked") }
native['checked'] = 'checked'
elsif tag_name == 'input' and type == 'checkbox'
if value && !native['checked']
native['checked'] = 'checked'
elsif !value && native['checked']
native.remove_attribute('checked')
end
elsif tag_name == 'input'
if (type == 'text' || type == 'password') && self[:maxlength]
value = value[0...self[:maxlength].to_i]
end
native['value'] = value.to_s
elsif tag_name == "textarea"
native.content = value.to_s
end
end
def select_option
if select_node['multiple'] != 'multiple'
select_node.find(".//option[@selected]").each { |node| node.native.remove_attribute("selected") }
end
native["selected"] = 'selected'
end
def unselect_option
if select_node['multiple'] != 'multiple'
raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
end
native.remove_attribute('selected')
end
def click
if tag_name == 'a'
method = self["data-method"] || :get
driver.follow(method, self[:href].to_s)
elsif (tag_name == 'input' and %w(submit image).include?(type)) or
((tag_name == 'button') and type.nil? or type == "submit")
Capybara::RackTest::Form.new(driver, form).submit(self)
end
end
def tag_name
native.node_name
end
def visible?
string_node.visible?
end
def checked?
string_node.checked?
end
def selected?
string_node.selected?
end
def path
native.path
end
def find(locator)
native.xpath(locator).map { |n| self.class.new(driver, n) }
end
private
def string_node
@string_node ||= Capybara::Node::Simple.new(native)
end
# a reference to the select node if this is an option node
def select_node
find('./ancestor::select').first
end
def type
native[:type]
end
def form
native.ancestors('form').first
end
end

View File

@ -51,6 +51,44 @@ shared_examples_for "session" do
end
end
describe '#reset!', :focus => true do
it "removes cookies" do
@session.visit('/set_cookie')
@session.visit('/get_cookie')
@session.body.should include('test_cookie')
@session.reset!
@session.visit('/get_cookie')
@session.body.should_not include('test_cookie')
end
it "resets current host" do
@session.visit('http://capybara-testapp.heroku.com')
@session.current_host.should == 'http://capybara-testapp.heroku.com'
@session.reset!
@session.current_host.should be_nil
end
it "resets current path" do
@session.visit('/with_html')
@session.current_path.should == '/with_html'
@session.reset!
@session.current_path.should == ""
end
it "resets page body" do
@session.visit('/with_html')
@session.body.should include('This is a test')
@session.find('.//h1').text.should include('This is a test')
@session.reset!
@session.body.should_not include('This is a test')
@session.should have_no_selector('.//h1')
end
end
it_should_behave_like "all"
it_should_behave_like "first"
it_should_behave_like "attach_file"

View File

@ -8,7 +8,7 @@ describe Capybara::Session do
describe '#driver' do
it "should be a rack test driver" do
@session.driver.should be_an_instance_of(Capybara::Driver::RackTest)
@session.driver.should be_an_instance_of(Capybara::RackTest::Driver)
end
end
@ -22,7 +22,7 @@ describe Capybara::Session do
it "should use data-method if available" do
@session.visit "/with_html"
@session.click_link "A link with data-method"
@session.body.should == 'The requested object was deleted'
@session.body.should include('The requested object was deleted')
end
end