diff --git a/.gitignore b/.gitignore index f07c0b3..1e26df0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ coverage +.DS_Store pkg doc ri -email.txt \ No newline at end of file +email.txt diff --git a/README.txt b/README.txt index df8ec0e..420384c 100644 --- a/README.txt +++ b/README.txt @@ -65,7 +65,10 @@ A test written with Webrat can handle these changes to these without any modific Install ------- -To install the latest release: +* Rails >= 1.2.6 or Merb edge +* Hpricot >= 0.6 +* Rails integration tests in Test::Unit _or_ +* RSpec stories (using an RSpec version >= revision 2997) sudo gem install webrat @@ -75,6 +78,13 @@ In your stories/helper.rb: You could also unpack the gem into vendor/plugins. +To avoid losing sessions, you need this in environments/test.rb: + +Merb::Config.use do |c| + c[:session_store] = 'memory' +end + +== HISTORY: Requirements ------------ diff --git a/lib/boot_merb.rb b/lib/boot_merb.rb new file mode 100644 index 0000000..b0500db --- /dev/null +++ b/lib/boot_merb.rb @@ -0,0 +1,127 @@ +module Webrat + module MerbTest + + #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. + # + #We have to preserve cookies like this, or the session is lost. + # + #While (in a web application) a PUT is modelled as a POST with a parameter _method, + #this close to the metal we need to make sure that we actually hit the underlying 'put' method, + #so we rewrite 'method'. + def request_via_redirect(method,path,parameters={},headers={}) + method = parameters["_method"] if !parameters["_method"].blank? + mycookies = defined?(@controller) ? @controller.cookies : nil #will be nil if no requests yet + begin + @controller=self.send(method, path, parameters, headers) do |new_controller| + new_controller.cookies = mycookies + end + rescue => exception + raise unless exception.kind_of?(Merb::ControllerExceptions::Base) + #Now we want to go one level below 'post' to build the request ourselves, then send it to the controller + exception_klass = exception.class + klass = ::Exceptions rescue Merb::Controller + request = fake_request + request.params[:exception] = exception + request.params[:action] = exception_klass.name + @controller=dispatch_request(request, klass, exception_klass.name) + end + + follow_redirect! while redirect? + status + end + + def get_via_redirect(path, parameters = {}, headers = {}) + request_via_redirect(:get,path,parameters,headers) + end + + def put_via_redirect(path, parameters = {}, headers = {}) + request_via_redirect(:put,path,parameters,headers) + end + + def post_via_redirect(path, parameters = {}, headers = {}) + request_via_redirect(:post,path,parameters,headers) + end + + def delete_via_redirect(path, parameters = {}, headers = {}) + request_via_redirect(:delete,path,parameters,headers) + end + + def follow_redirect! + mycookies = @controller.cookies rescue nil + @controller=get @controller.headers["Location"] do |new_controller| + new_controller.cookies=mycookies + end + 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 +end + +class Application < Merb::Controller + def cookies=(newcookies) + @_cookies = newcookies + 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") +require File.join(File.dirname(__FILE__), "merb_support", "support.rb") + + + diff --git a/lib/boot_rails.rb b/lib/boot_rails.rb new file mode 100644 index 0000000..1954ca7 --- /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 0000000..26e0814 --- /dev/null +++ b/lib/merb_support/indifferent_access.rb @@ -0,0 +1,125 @@ +# 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 + + diff --git a/lib/merb_support/param_parser.rb b/lib/merb_support/param_parser.rb new file mode 100644 index 0000000..534e1ff --- /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/support.rb b/lib/merb_support/support.rb new file mode 100644 index 0000000..f7c24ee --- /dev/null +++ b/lib/merb_support/support.rb @@ -0,0 +1,12 @@ +class Hash + def with_indifferent_access + hash = HashWithIndifferentAccess.new(self) + hash.default = self.default + hash + end +end +class NilClass + def to_param + nil + end +end 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 0000000..8e668c7 --- /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 dffddac..fbddb7c 100644 --- a/lib/webrat.rb +++ b/lib/webrat.rb @@ -1,5 +1,8 @@ module Webrat - VERSION = '0.2.1' + VERSION = '0.2.2' + def self.root + defined?(RAILS_ROOT) ? RAILS_ROOT : Merb.root + end end require "rubygems" @@ -7,3 +10,9 @@ require "active_support" require File.dirname(__FILE__) + "/webrat/core" require File.dirname(__FILE__) + "/webrat/rails" if defined?(RAILS_ENV) + +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/core/field.rb b/lib/webrat/core/field.rb index c9ea46b..2621c9e 100644 --- a/lib/webrat/core/field.rb +++ b/lib/webrat/core/field.rb @@ -3,18 +3,17 @@ module Webrat def self.class_for_element(element) if element.name == "input" - if %w[submit image].include?(element["type"]) + if %w[submit image button].include?(element["type"]) field_class = "button" else - field_class = element["type"] || "text" + 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) @@ -108,10 +107,10 @@ module Webrat def param_parser if defined?(CGIMethods) CGIMethods - else - require "action_controller" - require "action_controller/integration" + elsif defined?(ActionController::AbstractRequest) ActionController::AbstractRequest + else + Webrat::ParamParser #used for Merb end end @@ -141,6 +140,14 @@ module Webrat def matches_value?(value) @element["value"] =~ /^\W*#{Regexp.escape(value.to_s)}/i || matches_text?(value) || matches_alt?(value) end + + def matches_id?(id) + @element["id"] =~ /^\W*#{Regexp.escape(id.to_s)}/i + end + + def matches_caption?(value) + @element.innerHTML =~ /^\W*#{Regexp.escape(value.to_s)}/i + end def to_param return nil if @value.nil? diff --git a/lib/webrat/core/form.rb b/lib/webrat/core/form.rb index 60a5c07..33f7496 100644 --- a/lib/webrat/core/form.rb +++ b/lib/webrat/core/form.rb @@ -33,10 +33,19 @@ module Webrat possible_buttons = fields_by_type([ButtonField]) + possible_buttons.each do |possible_button| + return possible_button if possible_button.matches_id?(value) + end + possible_buttons.each do |possible_button| return possible_button if possible_button.matches_value?(value) end + #If nothing matched on value, try by name. + possible_buttons.each do |possible_button| + return possible_button if possible_button.matches_caption?(value) + end + nil end @@ -45,7 +54,7 @@ module Webrat @fields = [] - (@element / "button, input, textarea, select").each do |field_element| + (@element / "input, textarea, select, button").each do |field_element| @fields << Field.class_for_element(field_element).new(self, field_element) end @@ -123,7 +132,7 @@ module Webrat def merge_hash_values(a, b) # :nodoc: a.keys.each do |k| if b.has_key?(k) - case [a[k], b[k]].map(&:class) + case [a[k].class, b[k].class] when [Hash, Hash] a[k] = merge_hash_values(a[k], b[k]) b.delete(k) diff --git a/lib/webrat/core/logging.rb b/lib/webrat/core/logging.rb index e2be0c4..a77b6fa 100644 --- a/lib/webrat/core/logging.rb +++ b/lib/webrat/core/logging.rb @@ -3,12 +3,14 @@ module Webrat def debug_log(message) # :nodoc: return unless logger - logger.debug(message) + 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/lib/webrat/core/scope.rb b/lib/webrat/core/scope.rb index 445c1e9..3b96c12 100644 --- a/lib/webrat/core/scope.rb +++ b/lib/webrat/core/scope.rb @@ -82,10 +82,22 @@ module Webrat # along with the form. An optional content_type may be given. # # Example: +<<<<<<< HEAD:lib/webrat/core/scope.rb # attaches_file "Resume", "/path/to/the/resume.txt" # attaches_file "Photo", "/path/to/the/image.png", "image/png" def attaches_file(id_or_name_or_label, path, content_type = nil) find_field(id_or_name_or_label, FileField).set(path, content_type) +======= + # save_and_open + def save_and_open + return unless File.exist?(Webrat.root + "/tmp") + + filename = "webrat-#{Time.now.to_i}.html" + File.open(Webrat.root + "/tmp/#{filename}", "w") do |f| + f.write response.body + end + `open tmp/#{filename}` +>>>>>>> 300880db2f0d50a3e2d7b171eb9745cb50e1c534:lib/webrat/page.rb end alias_method :attach_file, :attaches_file diff --git a/spec/api/fills_in_spec.rb b/spec/api/fills_in_spec.rb index b13b2cf..eced1f1 100644 --- a/spec/api/fills_in_spec.rb +++ b/spec/api/fills_in_spec.rb @@ -144,6 +144,18 @@ describe "fills_in" do @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 it "should work with symbols" do @session.response_body = <<-EOS diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eff88ee..c4370c3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -4,13 +4,12 @@ require "spec" # gem install redgreen for colored test output begin require "redgreen" unless ENV['TM_CURRENT_LINE']; rescue LoadError; end -require "active_support" - -silence_warnings do - require "action_controller" - require "action_controller/integration" +if ["rails","merb"].include?(ENV["TEST_MODE"]) + require File.join(File.dirname(__FILE__), "helper_#{ENV["TEST_MODE"]}.rb") +else + raise "Please set the environment variable TEST_MODE to either 'rails' or 'merb'." end - + require File.expand_path(File.dirname(__FILE__) + "/../lib/webrat") require File.expand_path(File.dirname(__FILE__) + "/../lib/webrat/rails") require File.dirname(__FILE__) + "/fakes/test_session" diff --git a/test/helper_merb.rb b/test/helper_merb.rb new file mode 100644 index 0000000..82f54fe --- /dev/null +++ b/test/helper_merb.rb @@ -0,0 +1,11 @@ +require 'merb-core' +require 'merb_stories' +module Merb + module Test + class RspecStory + def flunk(message) + raise message + end + end + end +end diff --git a/test/helper_rails.rb b/test/helper_rails.rb new file mode 100644 index 0000000..8c0e9b4 --- /dev/null +++ b/test/helper_rails.rb @@ -0,0 +1,11 @@ +require "active_support" +silence_warnings do + require "action_controller" + require "action_controller/integration" +end + +class ActionController::Integration::Session + def flunk(message) + raise message + end +end \ No newline at end of file