diff --git a/README.txt b/README.txt index 8467c0c..e050010 100644 --- a/README.txt +++ b/README.txt @@ -59,6 +59,13 @@ tests to break unnecessarily as your application evolves: A test written with Webrat can handle these changes to these without any modifications. +=== Merb +To avoid losing sessions, you need this in environments/test.rb: + +Merb::Config.use do |c| + c[:session_store] = 'memory' +end + === Install To install the latest release: diff --git a/Rakefile b/Rakefile index 4838cb7..21a2275 100644 --- a/Rakefile +++ b/Rakefile @@ -35,25 +35,36 @@ def remove_task(task_name) Rake.application.remove_task(task_name) end +def set_file_list + if ENV['TEST_MODE'] == "merb" + list = FileList['spec/**/*_spec.rb'] + list = list.find_all do |file| !file.match("rails") end + return list + else + return FileList['spec/**/*_spec.rb'] + end +end + remove_task "test" remove_task "test_deps" desc "Run all specs in spec directory" Spec::Rake::SpecTask.new do |t| t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""] - t.spec_files = FileList['spec/**/*_spec.rb'] + t.spec_files = set_file_list end desc "Run all specs in spec directory with RCov" Spec::Rake::SpecTask.new(:rcov) do |t| t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""] - t.spec_files = FileList['spec/**/*_spec.rb'] + t.spec_files = set_file_list t.rcov = true t.rcov_opts = lambda do IO.readlines(File.dirname(__FILE__) + "/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten end end + require 'spec/rake/verify_rcov' RCov::VerifyTask.new(:verify_rcov => :rcov) do |t| t.threshold = 96.2 # Make sure you have rcov 0.7 or higher! diff --git a/lib/webrat.rb b/lib/webrat.rb index 8f11582..76acd40 100644 --- a/lib/webrat.rb +++ b/lib/webrat.rb @@ -6,3 +6,4 @@ require "rubygems" require File.dirname(__FILE__) + "/webrat/core" require File.dirname(__FILE__) + "/webrat/rails" if defined?(RAILS_ENV) +require File.dirname(__FILE__) + "/webrat/merb" if defined?(Merb) diff --git a/lib/webrat/core/logging.rb b/lib/webrat/core/logging.rb index e2be0c4..0d1a5fd 100644 --- a/lib/webrat/core/logging.rb +++ b/lib/webrat/core/logging.rb @@ -9,6 +9,8 @@ module Webrat def logger # :nodoc: if defined? RAILS_DEFAULT_LOGGER RAILS_DEFAULT_LOGGER + elsif defined? Merb + Merb.logger else nil end diff --git a/lib/webrat/merb.rb b/lib/webrat/merb.rb new file mode 100644 index 0000000..992c0ba --- /dev/null +++ b/lib/webrat/merb.rb @@ -0,0 +1,45 @@ +module Webrat + class Session + include Merb::Test::RequestHelper + + attr_reader :response + + def get(url, data, headers = nil) + do_request(url, data, headers, "GET") + end + + def post(url, data, headers = nil) + do_request(url, data, headers, "POST") + end + + def put(url, data, headers = nil) + do_request(url, data, headers, "PUT") + end + + def delete(url, data, headers = nil) + do_request(url, data, headers, "DELETE") + end + + def response_body + @response.body.to_s + end + + def response_code + @response.status + end + + protected + def do_request(url, data, headers, method) + @response = request(url, :params => data, :headers => headers, :method => method) + self.get(@response.headers['Location'], nil, @response.headers) if @response.status == 302 + end + + end +end + +class Merb::Test::RspecStory + def browser + @browser ||= Webrat::Session.new + end +end + diff --git a/lib/webrat/merb/indifferent_access.rb b/lib/webrat/merb/indifferent_access.rb new file mode 100644 index 0000000..26e0814 --- /dev/null +++ b/lib/webrat/merb/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/webrat/merb/param_parser.rb b/lib/webrat/merb/param_parser.rb new file mode 100644 index 0000000..534e1ff --- /dev/null +++ b/lib/webrat/merb/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/webrat/merb/support.rb b/lib/webrat/merb/support.rb new file mode 100644 index 0000000..f7c24ee --- /dev/null +++ b/lib/webrat/merb/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/webrat/merb/url_encoded_pair_parser.rb b/lib/webrat/merb/url_encoded_pair_parser.rb new file mode 100644 index 0000000..8e668c7 --- /dev/null +++ b/lib/webrat/merb/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/spec/api/attaches_file_spec.rb b/spec/api/attaches_file_spec.rb index edf0e74..6adecba 100644 --- a/spec/api/attaches_file_spec.rb +++ b/spec/api/attaches_file_spec.rb @@ -1,5 +1,5 @@ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper") - +unless ENV["TEST_MODE"] == "merb" #TODO - Rob describe "attaches_file" do before do @session = Webrat::TestSession.new @@ -70,3 +70,4 @@ describe "attaches_file" do @session.clicks_button end end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 297d4ed..feb04ac 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,16 +5,18 @@ require "spec/interop/test" # gem install redgreen for colored test output begin require "redgreen" unless ENV['TM_CURRENT_LINE']; rescue LoadError; end -require "active_support" +require File.expand_path(File.dirname(__FILE__) + "/../lib/webrat") +require File.dirname(__FILE__) + "/fakes/test_session" -silence_warnings do - require "action_controller" - require "action_controller/integration" +if ["rails","merb"].include?(ENV["TEST_MODE"]) + require File.join(File.dirname(__FILE__), "webrat", "#{ENV["TEST_MODE"]}", "helper.rb") +else + puts "Assuming test mode is Rails... for Merb set TEST_MODE=merb and rerun." + ENV["TEST_MODE"] = 'rails' + require File.join(File.dirname(__FILE__), "webrat", "#{ENV["TEST_MODE"]}", "helper.rb") 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" + Spec::Runner.configure do |config| # Nothing to configure yet diff --git a/spec/webrat/merb/helper.rb b/spec/webrat/merb/helper.rb new file mode 100644 index 0000000..ca55fd2 --- /dev/null +++ b/spec/webrat/merb/helper.rb @@ -0,0 +1,2 @@ +require 'merb-core' +require File.expand_path(File.dirname(__FILE__) + "/../../../lib/webrat/merb") \ No newline at end of file diff --git a/spec/webrat/rails/helper.rb b/spec/webrat/rails/helper.rb new file mode 100644 index 0000000..05d8068 --- /dev/null +++ b/spec/webrat/rails/helper.rb @@ -0,0 +1,7 @@ +require "active_support" + +silence_warnings do + require "action_controller" + require "action_controller/integration" +end +require File.expand_path(File.dirname(__FILE__) + "/../../../lib/webrat/rails")