diff --git a/.gitignore b/.gitignore index 1ff7516..c7dbe76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ coverage +.DS_Store pkg doc ri diff --git a/History.txt b/History.txt index d9ac11e..b952507 100644 --- a/History.txt +++ b/History.txt @@ -2,42 +2,50 @@ * Major enhancements + * Added Merb support (Gwyn Morfey, Jeremy Burks, Rob Kaufman) * Added #basic_auth(user, pass) to support HTTP Basic Auth (Aslak Hellesøy) * Added support for Sinatra and Rack (Aslak Hellesøy) * Added #within for manipulating the current page within a selector scope - * Add should_see and should_not_see for verifying HTML response bodys * Add support for simulating SSL requests (Luke Melia) - * Add support for file fields via #attaches_file method (Patch from Kyle Hargraves) - * Support relative links, including href="?foo=bar" (Patch from Kyle Hargraves) - * Separated Rails-specific code from the Webrat core to make it easier to use Webrat with other environments - * Alias visits as visit, clicks_link as click_link, etc. for better readability - * Raise error when trying to interact with a disabled form element (Luke Melia) - * Don't send disabled form elements to the server (Patch from Nicholas A. Evans) + * Add support for file fields via #attaches_file method (Kyle Hargraves) * Minor enhancements - * Add support for redirect_to :back by sending HTTP_REFERER headers (Patch from Hendrik Volkmer) + * Add Webrat.root method for cross-framework support (Krzysztof Zylawy) + * Add #select_date for quickly filling out Rails-style date fields (Alex Lang) + * Support selecting options by their values (Alex Lang) + * Support for clicking areas of an image map (Alex Lang) + * Add should_see and should_not_see for verifying HTML response bodys + * Support relative links, including href="?foo=bar" (Kyle Hargraves) + * Separated Rails-specific code from the Webrat core to make it easier to use Webrat with other environments + * Alias visits as visit, clicks_link as click_link, etc. for better readability + * Raise error when trying to interact with a disabled form element (Luke Melia) + * Don't send disabled form elements to the server (Nicholas A. Evans) + * Display response body when the page load is not successful (David Leal) + * CGI escape form field values (Miha Filej) + * Add support for redirect_to :back by sending HTTP_REFERER headers (Hendrik Volkmer) * Expose current DOM (as an Hpricot object) as #current_dom * Add support for disabling JavaScript when clicking a link to enable testing of both JS and non-JS implementations (Luke Melia and Bryan Helmkamp) - * Support  's as spaces in matching link text (Patch from Luke Melia) - * Support button elements (Patch from Nick Sieger) - * Support matching select options by regexp (Patch from Kyle Hargraves) + * Support  's as spaces in matching link text (Luke Melia) + * Support button elements (Nick Sieger) + * Support matching select options by regexp (Kyle Hargraves) * save_and_open_page rewrites css and image references to provide a friendlier debugging experience (Luke Melia) - * Added support for matching alt attributes in fields (primarly for clicks_button) (Patch from Aaron Quint) - * Support '&' in submitted values (Patch from Kyle Hargraves) - * Support clicking links by title (Patch from Dan Barry) - * Added missing spec for clicking image buttons (Patch from Tim Harper) + * Added support for matching alt attributes in fields (primarly for clicks_button) (Aaron Quint) + * Support '&' in submitted values (Kyle Hargraves) + * Support clicking links by title (Dan Barry) + * Added missing spec for clicking image buttons (Tim Harper) * Switched tests to specs, and from Mocha to RSpec's mocking library - * Add support to click_button for IDs (Patch from Gwyn Morfey) - * Miscellaneous core refactorings (Patch from Jan Suchal) + * Add support to click_button for IDs (Gwyn Morfey) + * Miscellaneous core refactorings (Jan Suchal) * Bug fixes - * Don't open blank pages in the browser (Patch from Kyle Hargraves) - * Support radio buttons with multiple labels (Patch from Dan Barry) - * Fix load order bug on some platforms (Patch from Ismael Celis) - * Fix bug with empty select list option (Patch from Kyle Hargraves) + * Fix initialization of WWW::Mechanize (Derek Kastner) + * Don't open blank pages in the browser (Kyle Hargraves) + * Support radio buttons with multiple labels (Dan Barry) + * Fix load order bug on some platforms (Ismael Celis) + * Fix bug with empty select list option (Kyle Hargraves) * Fix regression of not sending default values in password fields * Don't explode if encountering inputs with no type attribute (assume text) * Fix bug where choosing a radio button in a series with a default submitted the incorrect field value (Luke Melia) @@ -50,29 +58,29 @@ * Add radio button support via #chooses method * Add basic support for Rails-generated JavaScript link tags * Add support for checkboxes (Patches from Kyle Hargraves and Jarkko Laine) - * Add support for textarea fields (Patch from Sacha Schlegel) + * Add support for textarea fields (Sacha Schlegel) * 8 Minor enhancements - * Added reloads method to reload the page (Patch from Kamal Fariz Mahyuddi) - * Prevent making a request if clicking on local anchor link (Patch from Kamal Fariz Mahyuddi) + * Added reloads method to reload the page (Kamal Fariz Mahyuddi) + * Prevent making a request if clicking on local anchor link (Kamal Fariz Mahyuddi) * Added clicks_link_within(selector, link_text), allowing restricting link search - to within a given css selector (Patch from Luke Melia) - * Allow specifying the input name/label when doing a select (Patch from David Chelimsky) - * Raise a specific exception if the developer tries to manipulate form elements before loading a page (Patch from James Deville) - * Add support for alternate POST, PUT and DELETE link clicking (Patch from Kyle Hargraves) - * Change clicks_link to find the shortest matching link (Patch from Luke Melia) + to within a given css selector (Luke Melia) + * Allow specifying the input name/label when doing a select (David Chelimsky) + * Raise a specific exception if the developer tries to manipulate form elements before loading a page (James Deville) + * Add support for alternate POST, PUT and DELETE link clicking (Kyle Hargraves) + * Change clicks_link to find the shortest matching link (Luke Melia) * Improve matching for labels in potentially ambiguous cases * 7 Bug fixes - * Fix incorrect serializing of collection inputs, i.e. name contains [] (Patch from Kamal Fariz Mahyuddi) - * Serialize empty text field values just like browsers (Patch from Kamal Fariz Mahyuddi) - * Quick fix to avoid @dom not initialized warnings (Patch from Kamal Fariz Mahyuddi) - * Docfix: bad reference to #select method in README (Patch from Luke Melia) + * Fix incorrect serializing of collection inputs, i.e. name contains [] (Kamal Fariz Mahyuddi) + * Serialize empty text field values just like browsers (Kamal Fariz Mahyuddi) + * Quick fix to avoid @dom not initialized warnings (Kamal Fariz Mahyuddi) + * Docfix: bad reference to #select method in README (Luke Melia) * Ensure Rails-style checkboxes work properly (checkboxes followed by a hidden input with the same name) - * Fix Edge Rails (a.k.a. 2.0 RC) compatibility (Patch from David Chelimsky) - * Support param hashes nested more than one level (Patch from David Chelimsky) + * Fix Edge Rails (a.k.a. 2.0 RC) compatibility (David Chelimsky) + * Support param hashes nested more than one level (David Chelimsky) == 0.1.0 / 2007-11-28 diff --git a/Manifest.txt b/Manifest.txt index 4e749b6..08b0319 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -8,6 +8,7 @@ init.rb install.rb lib/webrat.rb lib/webrat/core.rb +lib/webrat/core/area.rb lib/webrat/core/assertions.rb lib/webrat/core/field.rb lib/webrat/core/flunk.rb @@ -20,22 +21,38 @@ lib/webrat/core/select_option.rb lib/webrat/core/session.rb lib/webrat/mechanize.rb lib/webrat/mechanize/mechanize_session.rb +lib/webrat/merb.rb +lib/webrat/merb/indifferent_access.rb +lib/webrat/merb/param_parser.rb +lib/webrat/merb/support.rb +lib/webrat/merb/url_encoded_pair_parser.rb +lib/webrat/rack/rack_session.rb lib/webrat/rails.rb lib/webrat/rails/rails_session.rb lib/webrat/rails/redirect_actions.rb lib/webrat/rails/session.rb lib/webrat/selenium.rb +lib/webrat/selenium/location_strategy_javascript/button.js +lib/webrat/selenium/location_strategy_javascript/label.js +lib/webrat/selenium/location_strategy_javascript/webrat.js +lib/webrat/selenium/location_strategy_javascript/webratlink.js +lib/webrat/selenium/location_strategy_javascript/webratlinkwithin.js +lib/webrat/selenium/location_strategy_javascript/webratselectwithoption.js lib/webrat/selenium/selenium_session.rb +lib/webrat/sinatra/sinatra_session.rb mechanize_spike.rb selenium_spike.rb spec/api/attaches_file_spec.rb +spec/api/basic_auth_spec.rb spec/api/checks_spec.rb spec/api/chooses_spec.rb +spec/api/clicks_area_spec.rb spec/api/clicks_button_spec.rb spec/api/clicks_link_spec.rb spec/api/fills_in_spec.rb spec/api/reloads_spec.rb spec/api/save_and_open_spec.rb +spec/api/selects_date_spec.rb spec/api/selects_spec.rb spec/api/should_not_see_spec.rb spec/api/should_see_spec.rb @@ -48,5 +65,8 @@ spec/spec.opts spec/spec_helper.rb spec/webrat/core/logging_spec.rb spec/webrat/core/session_spec.rb +spec/webrat/mechanize/mechanize_session_spec.rb +spec/webrat/merb/helper.rb +spec/webrat/rails/helper.rb spec/webrat/rails/rails_session_spec.rb webrat.gemspec 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 75c8c69..21a2275 100644 --- a/Rakefile +++ b/Rakefile @@ -35,28 +35,39 @@ 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 = 97.3 # Make sure you have rcov 0.7 or higher! + t.threshold = 96.2 # Make sure you have rcov 0.7 or higher! end remove_task "default" diff --git a/lib/webrat.rb b/lib/webrat.rb index 8f11582..af0c4dc 100644 --- a/lib/webrat.rb +++ b/lib/webrat.rb @@ -1,8 +1,12 @@ module Webrat - VERSION = '0.2.1' + VERSION = '0.2.2' + def self.root + defined?(RAILS_ROOT) ? RAILS_ROOT : Merb.root + end end 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/area.rb b/lib/webrat/core/area.rb new file mode 100644 index 0000000..3812654 --- /dev/null +++ b/lib/webrat/core/area.rb @@ -0,0 +1,44 @@ +module Webrat + class Area + + def initialize(session, element) + @session = session + @element = element + end + + def click(method = nil, options = {}) + @session.request_page(absolute_href, :get, {}) + end + + def matches_text?(id_or_title) + matcher = /#{Regexp.escape(id_or_title.to_s)}/i + title =~ matcher || id =~ matcher + end + + protected + + def href + @element["href"] + end + + def title + @element["title"] + end + + def id + @element["id"] + end + + + def absolute_href + if href =~ /^\?/ + "#{@session.current_url}#{href}" + elsif href !~ %r{^https?://[\w|.]+(/.*)} && (href !~ /^\//) + "#{@session.current_url}/#{href}" + else + href + end + end + + end +end \ No newline at end of file diff --git a/lib/webrat/core/field.rb b/lib/webrat/core/field.rb index 107bd66..42710cc 100644 --- a/lib/webrat/core/field.rb +++ b/lib/webrat/core/field.rb @@ -1,3 +1,5 @@ +require "cgi" + module Webrat class Field @@ -6,15 +8,14 @@ module Webrat if %w[submit image].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}" + raise "Invalid field element: #{element.inspect}" end def initialize(form, element) @@ -30,11 +31,11 @@ module Webrat end def matches_id?(id) - @element["id"] == id.to_s + matches?(self.id, id) end def matches_name?(name) - @element["name"] == name.to_s + matches?(self.name, name) end def matches_label?(label_text) @@ -56,10 +57,9 @@ module Webrat def to_param return nil if disabled? - value = @value.to_s.gsub('&', '%26') - param_parser.parse_query_parameters("#{name}=#{value}") + param_parser.parse_query_parameters("#{name}=#{escaped_value}") end - + def set(value) @value = value end @@ -70,6 +70,14 @@ module Webrat protected + def matches?(string, string_or_regex) + if string_or_regex.is_a?(Regexp) + string_or_regex.match string + else + string == string_or_regex + end + end + def id @element["id"] end @@ -78,6 +86,10 @@ module Webrat @element["name"] end + def escaped_value + CGI.escape(@value.to_s) + end + def labels @labels ||= label_elements.map { |element| Label.new(self, element) } end @@ -109,10 +121,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 @@ -138,10 +150,6 @@ module Webrat def matches_text?(text) @element.innerHTML =~ /#{Regexp.escape(text.to_s)}/i end - - # def matches_id?(id) - # @element["id"] =~ /^\W*#{Regexp.escape(id.to_s)}/i - # end def matches_value?(value) @element["value"] =~ /^\W*#{Regexp.escape(value.to_s)}/i || matches_text?(value) || matches_alt?(value) @@ -172,7 +180,7 @@ module Webrat else checkbox_with_same_name = @form.find_field(name, CheckboxField) - if checkbox_with_same_name.to_param.nil? + if checkbox_with_same_name.to_param.blank? super else nil @@ -300,7 +308,7 @@ module Webrat class SelectField < Field def find_option(text) - options.detect { |o| o.matches_text?(text) } + options.detect { |o| o.matches_text?(text) || o.matches_value?(text)} end protected diff --git a/lib/webrat/core/form.rb b/lib/webrat/core/form.rb index 847b159..de36093 100644 --- a/lib/webrat/core/form.rb +++ b/lib/webrat/core/form.rb @@ -17,14 +17,12 @@ module Webrat nil end - def find_select_option(option_text) + def find_select_option(option_text, field_name_pattern = nil) select_fields = fields_by_type([SelectField]) - - select_fields.each do |select_field| + select_fields.select{|field| field_name_pattern.nil? || field.matches_name?(field_name_pattern) || field.matches_id?(field_name_pattern)}.each do |select_field| result = select_field.find_option(option_text) return result if result end - nil end @@ -105,7 +103,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], b[k]].map{|value| value.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..6634e96 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 f74727d..600a7b0 100644 --- a/lib/webrat/core/scope.rb +++ b/lib/webrat/core/scope.rb @@ -12,6 +12,22 @@ module Webrat @selector = selector end + def selects_date(date_string, options = {}) + id_or_name = options[:from] + date = Date.parse date_string + + year_option = find_select_option(date.year.to_s, /#{id_or_name.to_s}.*1i/) + month_option = find_select_option(date.month.to_s, /#{id_or_name.to_s}.*2i/) + day_option = find_select_option(date.day.to_s, /#{id_or_name.to_s}.*3i/) + + flunk("Could not find date picker for #{date_string}") if year_option.nil? || month_option.nil? || day_option.nil? + year_option.choose + month_option.choose + day_option.choose + end + + alias_method :select_date, :selects_date + # Verifies an input field or textarea exists on the current page, and stores a value for # it which will be sent when the form is submitted. # @@ -91,6 +107,12 @@ module Webrat alias_method :attach_file, :attaches_file + def clicks_area(area_name) + find_area(area_name).click + end + + alias_method :click_area, :clicks_area + # Issues a request for the URL pointed to by a link on the current page, # follows any redirects, and verifies the final page load was successful. # @@ -200,10 +222,19 @@ module Webrat button = form.find_button(value) return button if button end - flunk("Could not find button #{value.inspect}") end + def find_area(area_name) + areas.select{|area| area.matches_text?(area_name)}.first || flunk("Could not find area with name #{area_name}") + end + + def areas + (dom / "area").map do |element| + Area.new(@session, element) + end + end + def find_link(text, selector = nil) matching_links = links_within(selector).select do |possible_link| possible_link.matches_text?(text) diff --git a/lib/webrat/core/select_option.rb b/lib/webrat/core/select_option.rb index c46224f..8df63dd 100644 --- a/lib/webrat/core/select_option.rb +++ b/lib/webrat/core/select_option.rb @@ -14,6 +14,10 @@ module Webrat end end + def matches_value?(value) + @element.attributes['value'] == value.to_s + end + def choose @select.raise_error_if_disabled @select.set(value) diff --git a/lib/webrat/core/session.rb b/lib/webrat/core/session.rb index 1577b1a..ab89e50 100644 --- a/lib/webrat/core/session.rb +++ b/lib/webrat/core/session.rb @@ -73,7 +73,7 @@ module Webrat end save_and_open_page if exception_caught? - flunk("Page load was not successful (Code: #{response_code.inspect})") unless success_code? + flunk("Page load was not successful (Code: #{response_code.inspect}):\n#{formatted_error}") unless success_code? @scope = nil @current_url = url @@ -141,12 +141,19 @@ module Webrat response_html.gsub(/"\/(stylesheets|images)/, doc_root + '/\1') end + # Subclasses can override this to show error messages without html + def formatted_error + response_body + end + def_delegators :current_scope, :fill_in, :fills_in def_delegators :current_scope, :check, :checks def_delegators :current_scope, :uncheck, :unchecks def_delegators :current_scope, :choose, :chooses def_delegators :current_scope, :select, :selects + def_delegators :current_scope, :select_date, :selects_date def_delegators :current_scope, :attach_file, :attaches_file + def_delegators :current_scope, :click_area, :clicks_area def_delegators :current_scope, :click_link, :clicks_link def_delegators :current_scope, :click_get_link, :clicks_get_link def_delegators :current_scope, :click_delete_link, :clicks_delete_link @@ -156,4 +163,4 @@ module Webrat def_delegators :current_scope, :should_see def_delegators :current_scope, :should_not_see end -end \ No newline at end of file +end diff --git a/lib/webrat/mechanize/mechanize_session.rb b/lib/webrat/mechanize/mechanize_session.rb index 2f48c17..7a817c4 100644 --- a/lib/webrat/mechanize/mechanize_session.rb +++ b/lib/webrat/mechanize/mechanize_session.rb @@ -2,6 +2,7 @@ module Webrat class MechanizeSession < Session def initialize(mechanize = WWW::Mechanize.new) + super() @mechanize = mechanize end diff --git a/lib/webrat/merb.rb b/lib/webrat/merb.rb new file mode 100644 index 0000000..9d1dc8d --- /dev/null +++ b/lib/webrat/merb.rb @@ -0,0 +1,49 @@ +Dir[File.join(File.dirname(__FILE__), "merb", "*.rb")].sort.each do |file| + require File.expand_path(file) +end + +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 && data.any?) ? data : nil, :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..0ee2e5f --- /dev/null +++ b/lib/webrat/merb/param_parser.rb @@ -0,0 +1,19 @@ +require "cgi" + +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..63c74f0 --- /dev/null +++ b/lib/webrat/merb/url_encoded_pair_parser.rb @@ -0,0 +1,96 @@ +require "cgi" +require "strscan" + +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/api/clicks_area_spec.rb b/spec/api/clicks_area_spec.rb new file mode 100644 index 0000000..c7fbc29 --- /dev/null +++ b/spec/api/clicks_area_spec.rb @@ -0,0 +1,81 @@ +require File.expand_path(File.dirname(__FILE__) + "/../spec_helper") + +describe "clicks_area" do + before do + @session = Webrat::TestSession.new + end + + it "should use get by default" do + @session.response_body = <<-EOS + + Berlin + + EOS + @session.should_receive(:get).with("/page", {}) + @session.clicks_area "Berlin" + end + + it "should assert valid response" do + @session.response_body = <<-EOS + + Berlin + + EOS + @session.response_code = 404 + lambda { @session.clicks_area "Berlin" }.should raise_error + end + + it "should fail if the area doesn't exist" do + @session.response_body = <<-EOS + + Berlin + + EOS + + lambda { + @session.clicks_area "Missing area" + }.should raise_error + end + + it "should not be case sensitive" do + @session.response_body = <<-EOS + + Berlin + + EOS + @session.should_receive(:get).with("/page", {}) + @session.clicks_area "berlin" + end + + + it "should follow relative links" do + @session.stub!(:current_url).and_return("/page") + @session.response_body = <<-EOS + + Berlin + + EOS + @session.should_receive(:get).with("/page/sub", {}) + @session.clicks_area "Berlin" + end + + it "should follow fully qualified local links" do + @session.response_body = <<-EOS + + Berlin + + EOS + @session.should_receive(:get).with("http://www.example.com/page", {}) + @session.clicks_area "Berlin" + end + + it "should follow query parameters" do + @session.response_body = <<-EOS + + Berlin + + EOS + @session.should_receive(:get).with("/page?foo=bar", {}) + @session.clicks_area "Berlin" + end +end diff --git a/spec/api/fills_in_spec.rb b/spec/api/fills_in_spec.rb index b13b2cf..226eeb7 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 + + it "should work if the input type is not set" do + @session.response_body = <<-EOS +
+ + +
+ EOS + @session.should_receive(:post).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 @@ -157,4 +169,17 @@ describe "fills_in" do @session.fills_in :email, :with => "foo@example.com" @session.clicks_button end + + it "should escape field values" do + @session.response_body = <<-EOS +
+ + + +
+ EOS + @session.should_receive(:post).with("/users", "user" => {"phone" => "+1 22 33"}) + @session.fills_in 'Phone', :with => "+1 22 33" + @session.clicks_button + end end diff --git a/spec/api/selects_date_spec.rb b/spec/api/selects_date_spec.rb new file mode 100644 index 0000000..cad52ff --- /dev/null +++ b/spec/api/selects_date_spec.rb @@ -0,0 +1,111 @@ +require File.expand_path(File.dirname(__FILE__) + "/../spec_helper") + +describe "date_selects" do + before do + @session = Webrat::TestSession.new + @example_date_select = <<-EOS +
+
+ + + + +
+ EOS + end + + + it "should fail if option not found" do + @session.response_body = @example_date_select + lambda { @session.selects_date "2008-07-13"}.should raise_error + end + + it "should fail if option not found in list specified by element name" do + @session.response_body = @example_date_select + lambda { @session.selects_date "2008-07-13", :from => "created_at" }.should raise_error + end + + it "should fail if specified list not found" do + @session.response_body = <<-EOS +
+ +
+ EOS + + lambda { @session.selects_date "2003-12-01", :from => "created_at" }.should raise_error + end + + it "should send value from option" do + @session.response_body = <<-EOS +
+
+ + + +
+ + + + +
+ EOS + @session.should_receive(:post).with("/login", "created_at(1i)" => "2003", 'created_at(2i)' => '12', 'created_at(3i)' => '1', "updated_at(1i)" => "", 'updated_at(2i)' => '', 'updated_at(3i)' => '') + @session.selects_date '2003-12-01', :from => "created_at" + @session.clicks_button + end + + it "should work without specifying the field name or label" do + @session.response_body = @example_date_select + @session.should_receive(:post).with("/login", "created_at(1i)" => "2003", 'created_at(2i)' => '12', 'created_at(3i)' => '1') + @session.selects_date '2003-12-01' + @session.clicks_button + end + + it "should correctly set day and month when there are the same options available" do + @session.response_body = <<-EOS +
+
+ + + + +
+ EOS + @session.should_receive(:post).with("/login", "created_at(1i)" => "2003", 'created_at(2i)' => '12', 'created_at(3i)' => '1') + @session.selects_date '2003-12-01' + @session.clicks_button + end + +end diff --git a/spec/api/selects_spec.rb b/spec/api/selects_spec.rb index d33afce..75e44d9 100644 --- a/spec/api/selects_spec.rb +++ b/spec/api/selects_spec.rb @@ -108,6 +108,18 @@ describe "selects" do @session.clicks_button end + it "should send value from option in list specified by value" do + @session.response_body = <<-EOS +
+ + +
+ EOS + @session.should_receive(:post).with("/login", "start_month" => "s1") + @session.selects "s1", :from => "start_month" + @session.clicks_button + end + it "should send value from option in list specified by label" do @session.response_body = <<-EOS
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 297d4ed..015d1e8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,17 +5,17 @@ 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" - -silence_warnings do - require "action_controller" - require "action_controller/integration" -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" +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 + Spec::Runner.configure do |config| # Nothing to configure yet end \ No newline at end of file diff --git a/spec/webrat/mechanize/mechanize_session_spec.rb b/spec/webrat/mechanize/mechanize_session_spec.rb new file mode 100644 index 0000000..f69dad2 --- /dev/null +++ b/spec/webrat/mechanize/mechanize_session_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path(File.dirname(__FILE__) + "/../../../lib/webrat/mechanize") + +describe Webrat::MechanizeSession do + before(:each) do + @mech = Webrat::MechanizeSession.new + end + + describe "headers method" do + it "should return empty headers for a newly initialized session" do + @mech.headers.should == {} + end + end +end \ No newline at end of file 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/merb/indifferent_access_spec.rb b/spec/webrat/merb/indifferent_access_spec.rb new file mode 100644 index 0000000..9a92c6d --- /dev/null +++ b/spec/webrat/merb/indifferent_access_spec.rb @@ -0,0 +1,48 @@ +require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper") +require File.expand_path(File.dirname(__FILE__) + "/helper") + +describe HashWithIndifferentAccess do + it "should not update constructor when not a hash" do + HashWithIndifferentAccess.should_receive(:update).never + HashWithIndifferentAccess.new('test') + end + + it "should get the default for key" do + h = HashWithIndifferentAccess.new(:test => 'me') + h.should_receive(:super).never + + h.default(:test).should == 'me' + end + + context "a hash with a test value applied" do + + setup do + @h = HashWithIndifferentAccess.new + @h[:test] = 'me' + end + + it "should assign a new value" do + @h[:test].should == 'me' + end + + it "should return true if asked for existing key" do + @h.key?(:test).should be_true + end + + it "should return array of values for keys" do + @h.values_at(:test).should == ['me'] + end + + it "should merge with another hash" do + another = HashWithIndifferentAccess.new(:value => 'test') + @h.merge(another).values_at(:test, :value).should == ['me','test'] + end + + it "should delete the key" do + @h.delete(:test) + @h.any?.should be_false + @h[:test].should be_nil + end + + end +end \ No newline at end of file diff --git a/spec/webrat/merb/session_spec.rb b/spec/webrat/merb/session_spec.rb new file mode 100644 index 0000000..237413d --- /dev/null +++ b/spec/webrat/merb/session_spec.rb @@ -0,0 +1,43 @@ +require File.expand_path(File.dirname(__FILE__) + "/../../spec_helper") +require File.expand_path(File.dirname(__FILE__) + "/helper") + +describe Webrat::Session do + + it "should not pass empty params if data is and empty hash" do + session = Webrat::Session.new + response = OpenStruct.new + response.status = 200 + session.should_receive(:request).with('url', {:params=> nil, :method=>"GET", :headers=>nil}).and_return(response) + session.get('url', {}, nil) + end + + %w{post put delete}.each do |request_method| + it "should call do request with method #{request_method.upcase} for a #{request_method} call" do + session = Webrat::Session.new + response = OpenStruct.new + response.status = 200 + + session.should_receive(:request).with('url', {:params=>nil, :method=>request_method.upcase, :headers=>nil}).and_return(response) + session.send(request_method, 'url', {}, nil) + end + end + + context "a session with a response" do + + setup do + @session = Webrat::Session.new + @response = OpenStruct.new + @response.status = 200 + @response.body = 'test response' + @session.instance_variable_set(:@response, @response) + end + + it "should return body of a request as a response_body" do + @session.response_body.should == @response.body + end + + it "should return status of a request as a response_code" do + @session.response_code.should == @response.status + end + end +end \ 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")