diff --git a/Manifest.txt b/Manifest.txt index 55ecc1f9..abdb8ef5 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -1,26 +1,3 @@ -.git/HEAD -.git/config -.git/description -.git/hooks/applypatch-msg -.git/hooks/commit-msg -.git/hooks/post-commit -.git/hooks/post-receive -.git/hooks/post-update -.git/hooks/pre-applypatch -.git/hooks/pre-commit -.git/hooks/pre-rebase -.git/hooks/update -.git/index -.git/info/exclude -.git/logs/HEAD -.git/logs/refs/heads/master -.git/logs/refs/remotes/origin/master -.git/objects/pack/pack-3243faf8e8f6f0e226adf39a2c2e8c19c18da6c4.idx -.git/objects/pack/pack-3243faf8e8f6f0e226adf39a2c2e8c19c18da6c4.pack -.git/refs/heads/master -.git/refs/remotes/origin/HEAD -.git/refs/remotes/origin/master -.gitignore History.txt MIT-LICENSE.txt Manifest.txt @@ -38,6 +15,11 @@ lib/webrat/logging.rb lib/webrat/page.rb lib/webrat/redirect_actions.rb lib/webrat/select_option.rb +lib/boot_merb.rb +lib/boot_rails.rb +lib/merb_support/indifferent_access.rb +lib/merb_support/param_parser.rb +lib/merb_support/url_encoded_pair_parser.rb test/checks_test.rb test/chooses_test.rb test/clicks_button_test.rb diff --git a/lib/boot_merb.rb b/lib/boot_merb.rb new file mode 100644 index 00000000..21c4a9b4 --- /dev/null +++ b/lib/boot_merb.rb @@ -0,0 +1,84 @@ +#In Merb, we have an RspecStory instead of an integration Session. +class Merb::Test::RspecStory + + #Our own redirect actions defined below, to deal with the fact that we need to store + #a controller reference. + + def current_page + @current_page ||= Webrat::Page.new(self) + end + + def current_page=(new_page) + @current_page = new_page + end + + # Issues a GET request for a page, follows any redirects, and verifies the final page + # load was successful. + # + # Example: + # visits "/" + def visits(*args) + @current_page = Webrat::Page.new(self, *args) + end + + def save_and_open_page + current_page.save_and_open + end + + [:reloads, :fills_in, :clicks_button, :selects, :chooses, :checks, :unchecks, :clicks_link, :clicks_link_within, :clicks_put_link, :clicks_get_link, :clicks_post_link, :clicks_delete_link].each do |method_name| + define_method(method_name) do |*args| + current_page.send(method_name, *args) + end + end + + #Session defines the following (used by webrat), but RspecStory doesn't. Merb's get/put/delete return a controller, + #which is where we get our status and response from. + def get_via_redirect(path, parameters = {}, headers = {}) + @controller=get path, parameters, headers + follow_redirect! while redirect? + status + end + def put_via_redirect(path, parameters = {}, headers = {}) + @controller=put path, parameters, headers + follow_redirect! while redirect? + status + end + def delete_via_redirect(path, parameters = {}, headers = {}) + @controller=delete path, parameters, headers + follow_redirect! while redirect? + status + end + + def follow_redirect! + @controller=get @controller.headers["Location"] + end + + def redirect? + [307, *(300..305)].include?(status) + end + + def status + @controller.status + end + + def response + @controller #things like @controller.body will work. + end + + def assert_response(resp) + if resp == :success + response.should be_successful + else + raise "assert_response #{resp.inspect} is not supported" + end + end +end + +#Other utilities used by Webrat that are present in Rails but not Merb. We can require heavy dependencies +#here because we're only loaded in Test mode. +require 'strscan' +require 'cgi' +require File.join(File.dirname(__FILE__), "merb_support", "param_parser.rb") +require File.join(File.dirname(__FILE__), "merb_support", "url_encoded_pair_parser.rb") +require File.join(File.dirname(__FILE__), "merb_support", "indifferent_access.rb") + diff --git a/lib/boot_rails.rb b/lib/boot_rails.rb new file mode 100644 index 00000000..1954ca74 --- /dev/null +++ b/lib/boot_rails.rb @@ -0,0 +1,39 @@ +module ActionController + module Integration + class Session + + unless instance_methods.include?("put_via_redirect") + include Webrat::RedirectActions + end + + def current_page + @current_page ||= Webrat::Page.new(self) + end + + def current_page=(new_page) + @current_page = new_page + end + + # Issues a GET request for a page, follows any redirects, and verifies the final page + # load was successful. + # + # Example: + # visits "/" + def visits(*args) + @current_page = Webrat::Page.new(self, *args) + end + + def save_and_open_page + current_page.save_and_open + end + + [:reloads, :fills_in, :clicks_button, :selects, :chooses, :checks, :unchecks, :clicks_link, :clicks_link_within, :clicks_put_link, :clicks_get_link, :clicks_post_link, :clicks_delete_link].each do |method_name| + define_method(method_name) do |*args| + current_page.send(method_name, *args) + end + end + + end + end +end + diff --git a/lib/merb_support/indifferent_access.rb b/lib/merb_support/indifferent_access.rb new file mode 100644 index 00000000..2213b091 --- /dev/null +++ b/lib/merb_support/indifferent_access.rb @@ -0,0 +1,138 @@ +# This class has dubious semantics and we only have it so that +# people can write params[:key] instead of params['key'] +# and they get the same value for both keys. + +class HashWithIndifferentAccess < Hash + def initialize(constructor = {}) + if constructor.is_a?(Hash) + super() + update(constructor) + else + super(constructor) + end + end + + def default(key = nil) + if key.is_a?(Symbol) && include?(key = key.to_s) + self[key] + else + super + end + end + + alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) + alias_method :regular_update, :update unless method_defined?(:regular_update) + + # + # Assigns a new value to the hash. + # + # Example: + # + # hash = HashWithIndifferentAccess.new + # hash[:key] = "value" + # + def []=(key, value) + regular_writer(convert_key(key), convert_value(value)) + end + + # + # Updates the instantized hash with values from the second. + # + # Example: + # + # >> hash_1 = HashWithIndifferentAccess.new + # => {} + # + # >> hash_1[:key] = "value" + # => "value" + # + # >> hash_2 = HashWithIndifferentAccess.new + # => {} + # + # >> hash_2[:key] = "New Value!" + # => "New Value!" + # + # >> hash_1.update(hash_2) + # => {"key"=>"New Value!"} + # + def update(other_hash) + other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } + self + end + + alias_method :merge!, :update + + # Checks the hash for a key matching the argument passed in + def key?(key) + super(convert_key(key)) + end + + alias_method :include?, :key? + alias_method :has_key?, :key? + alias_method :member?, :key? + + # Fetches the value for the specified key, same as doing hash[key] + def fetch(key, *extras) + super(convert_key(key), *extras) + end + + # Returns an array of the values at the specified indicies. + def values_at(*indices) + indices.collect {|key| self[convert_key(key)]} + end + + # Returns an exact copy of the hash. + def dup + HashWithIndifferentAccess.new(self) + end + + # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash + # Does not overwrite the existing hash. + def merge(hash) + self.dup.update(hash) + end + + # Removes a specified key from the hash. + def delete(key) + super(convert_key(key)) + end + + def stringify_keys!; self end + def symbolize_keys!; self end + def to_options!; self end + + # Convert to a Hash with String keys. + def to_hash + Hash.new(default).merge(self) + end + + protected + def convert_key(key) + key.kind_of?(Symbol) ? key.to_s : key + end + + def convert_value(value) + case value + when Hash + value.with_indifferent_access + when Array + value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e } + else + value + end + end +end + +module ActiveSupport #:nodoc: + module CoreExtensions #:nodoc: + module Hash #:nodoc: + module IndifferentAccess #:nodoc: + def with_indifferent_access + hash = HashWithIndifferentAccess.new(self) + hash.default = self.default + hash + end + end + end + end +end diff --git a/lib/merb_support/param_parser.rb b/lib/merb_support/param_parser.rb new file mode 100644 index 00000000..534e1ff2 --- /dev/null +++ b/lib/merb_support/param_parser.rb @@ -0,0 +1,17 @@ +module Webrat + class ParamParser + def self.parse_query_parameters(query_string) + return {} if query_string.blank? + + pairs = query_string.split('&').collect do |chunk| + next if chunk.empty? + key, value = chunk.split('=', 2) + next if key.empty? + value = value.nil? ? nil : CGI.unescape(value) + [ CGI.unescape(key), value ] + end.compact + + UrlEncodedPairParser.new(pairs).result + end + end +end \ No newline at end of file diff --git a/lib/merb_support/url_encoded_pair_parser.rb b/lib/merb_support/url_encoded_pair_parser.rb new file mode 100644 index 00000000..8e668c79 --- /dev/null +++ b/lib/merb_support/url_encoded_pair_parser.rb @@ -0,0 +1,93 @@ +class UrlEncodedPairParser < StringScanner #:nodoc: + attr_reader :top, :parent, :result + + def initialize(pairs = []) + super('') + @result = {} + pairs.each { |key, value| parse(key, value) } + end + + KEY_REGEXP = %r{([^\[\]=&]+)} + BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]} + + # Parse the query string + def parse(key, value) + self.string = key + @top, @parent = result, nil + + # First scan the bare key + key = scan(KEY_REGEXP) or return + key = post_key_check(key) + + # Then scan as many nestings as present + until eos? + r = scan(BRACKETED_KEY_REGEXP) or return + key = self[1] + key = post_key_check(key) + end + + bind(key, value) + end + + private + # After we see a key, we must look ahead to determine our next action. Cases: + # + # [] follows the key. Then the value must be an array. + # = follows the key. (A value comes next) + # & or the end of string follows the key. Then the key is a flag. + # otherwise, a hash follows the key. + def post_key_check(key) + if scan(/\[\]/) # a[b][] indicates that b is an array + container(key, Array) + nil + elsif check(/\[[^\]]/) # a[b] indicates that a is a hash + container(key, Hash) + nil + else # End of key? We do nothing. + key + end + end + + # Add a container to the stack. + def container(key, klass) + type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass) + value = bind(key, klass.new) + type_conflict! klass, value unless value.is_a?(klass) + push(value) + end + + # Push a value onto the 'stack', which is actually only the top 2 items. + def push(value) + @parent, @top = @top, value + end + + # Bind a key (which may be nil for items in an array) to the provided value. + def bind(key, value) + if top.is_a? Array + if key + if top[-1].is_a?(Hash) && ! top[-1].key?(key) + top[-1][key] = value + else + top << {key => value}.with_indifferent_access + push top.last + value = top[key] + end + else + top << value + end + elsif top.is_a? Hash + key = CGI.unescape(key) + parent << (@top = {}) if top.key?(key) && parent.is_a?(Array) + top[key] ||= value + return top[key] + else + raise ArgumentError, "Don't know what to do: top is #{top.inspect}" + end + + return value + end + + def type_conflict!(klass, value) + raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)" + end +end \ No newline at end of file diff --git a/lib/webrat.rb b/lib/webrat.rb index 4401f1a7..744fe10d 100644 --- a/lib/webrat.rb +++ b/lib/webrat.rb @@ -6,42 +6,8 @@ module Webrat VERSION = '0.2.1' end -module ActionController - module Integration - class Session - - unless instance_methods.include?("put_via_redirect") - include Webrat::RedirectActions - end - - def current_page - @current_page ||= Webrat::Page.new(self) - end - - def current_page=(new_page) - @current_page = new_page - end - - # Issues a GET request for a page, follows any redirects, and verifies the final page - # load was successful. - # - # Example: - # visits "/" - def visits(*args) - @current_page = Webrat::Page.new(self, *args) - end - - def save_and_open_page - current_page.save_and_open - end - - [:reloads, :fills_in, :clicks_button, :selects, :chooses, :checks, :unchecks, :clicks_link, :clicks_link_within, :clicks_put_link, :clicks_get_link, :clicks_post_link, :clicks_delete_link].each do |method_name| - define_method(method_name) do |*args| - current_page.send(method_name, *args) - end - end - - end - end +if defined?(Merb) + require File.join(File.dirname(__FILE__), "boot_merb.rb") +else + require File.join(File.dirname(__FILE__), "boot_rails.rb") end - diff --git a/lib/webrat/field.rb b/lib/webrat/field.rb index 34b70bd9..7c287981 100644 --- a/lib/webrat/field.rb +++ b/lib/webrat/field.rb @@ -6,15 +6,15 @@ def self.class_for_element(element) if %w[submit image].include?(element["type"]) field_class = "button" else - field_class = element["type"] + field_class = element["type"] || "text" #default type; 'type' attribute is not mandatory end else field_class = element.name end Webrat.const_get("#{field_class.capitalize}Field") - rescue NameError - raise "Invalid field element: #{element.inspect}" + #rescue NameError + # raise "Invalid field element: #{element.inspect}" end def initialize(form, element) @@ -92,8 +92,10 @@ def default_value def param_parser if defined?(CGIMethods) CGIMethods - else + elsif defined?(ActionController::AbstractRequest) ActionController::AbstractRequest + else + Webrat::ParamParser #used for Merb end end diff --git a/lib/webrat/logging.rb b/lib/webrat/logging.rb index 08e9870f..a77b6fac 100644 --- a/lib/webrat/logging.rb +++ b/lib/webrat/logging.rb @@ -3,12 +3,14 @@ module Logging def debug_log(message) # :nodoc: return unless logger - logger.debug + logger.debug message end def logger # :nodoc: if defined? RAILS_DEFAULT_LOGGER RAILS_DEFAULT_LOGGER + elsif defined? Merb + Merb.logger else nil end diff --git a/test/fills_in_test.rb b/test/fills_in_test.rb index 0ebb5672..19daf3f3 100644 --- a/test/fills_in_test.rb +++ b/test/fills_in_test.rb @@ -137,6 +137,18 @@ def test_should_work_with_full_input_names @session.fills_in "user[email]", :with => "foo@example.com" @session.clicks_button end + + def test_should_work_without_input_type + @response.stubs(:body).returns(<<-EOS) +
+ + +
+ EOS + @session.expects(:post_via_redirect).with("/login", "user" => {"email" => "foo@example.com"}) + @session.fills_in "user[email]", :with => "foo@example.com" + @session.clicks_button + end def test_should_work_with_symbols @response.stubs(:body).returns(<<-EOS)