module FormTestHelper module TagProxy def method_missing(method, *args) if tag.respond_to?(method) tag.send(method, *args) else super end end end class Form class FieldNotFoundError < RuntimeError; end class MissingSubmitError < RuntimeError; end include TagProxy attr_reader :tag attr_accessor :xhr def initialize(tag, testcase) @tag, @testcase = tag, testcase end # If you submit the form with JavaScript def submit_without_clicking_button path = self.action.blank? ? self.uri : self.action # If no action attribute on form, it submits to the same URI where the form was displayed params = {} fields.each {|field| params[field.name] = field.value unless field.value.nil? || field.value == [] || params[field.name] } # don't submit the nils, empty arrays, and fields already named # Convert arrays and hashes in param keys, since test processing doesn't do this automatically params = ActionController::UrlEncodedPairParser.new(params).result @testcase.make_request(request_method, path, params, self.uri, xhr) end # Submits the form. Raises an exception if no submit button is present. def submit(opts={}) raise MissingSubmitError, "Submit button not found in form" unless tag.select('input[type="submit"], input[type="image"], button[type="submit"]').any? @xhr = opts.delete(:xhr) fields_hash.update(opts) submit_without_clicking_button end def uri @testcase.instance_variable_get("@request").request_uri end def field_names fields.collect {|field| field.name } end def fields # Input, textarea, select, and button are valid field tags. Name is a required attribute. @fields ||= tag.select('input, textarea, select, button').reject {|field_tag| field_tag['name'].nil? }.group_by {|field_tag| field_tag['name'] }.collect do |name, field_tags| case field_tags.first['type'] when 'submit' FormTestHelper::Submit.new(field_tags) when 'checkbox' FormTestHelper::CheckBox.new(field_tags) when 'hidden' FormTestHelper::Hidden.new(field_tags) when 'radio' FormTestHelper::RadioButtonGroup.new(field_tags) else if field_tags.first.name == 'select' if field_tags.first['multiple'] # The multiple attribute is set FormTestHelper::SelectMultiple.new(field_tags) else FormTestHelper::Select.new(field_tags) end else FormTestHelper::Field.new(field_tags) end end end end def fields_hash @fields_hash ||= FieldsHash.new(ActionController::UrlEncodedPairParser.new(fields.collect {|field| [field.name, field] }).result) end # Accepts a block that can work with a single object (group of fields corresponding to a # single ActiveRecord object) # # Example: # form.with_object(:book) do |book| # book.name = 'Pickaxe' # book.category = 'Programming' # book.classic.check # end def with_object(object_name) yield self.send(object_name) end def find_field_by_name(field_name) field_name = field_name.to_s.gsub(/\[\]$/, '') # Strip any trailing empty square brackets matching_fields = self.fields.select {|field| field.name == field_name } return nil if matching_fields.empty? matching_fields.first end # Same as find_field_by_name but raises an exception if the field doesn't exist. def [](field_name) find_field_by_name(field_name) || raise(FieldNotFoundError, "Field named '#{field_name}' not found in form.") end def method_missing(method, *args) method = method.to_s if method.gsub!(/=$/, '') self[method].value = *args elsif fields_hash.has_key?(method) fields_hash[method].proxy else self[method].proxy end end def []=(field_name, value) self[field_name].value = value end def reset fields.each {|field| field.reset } end def action tag["action"] end def request_method hidden_method_field = self.find_field_by_name("_method") if hidden_method_field # PUT and DELETE hidden_method_field.value.to_sym elsif tag["method"] && !tag["method"].blank? # POST and GET tag["method"].to_sym else # No method specified in form tags :get end end end # A hash of fields to allow infinite nesting of fields named like 'person[address][street]' class FieldsHash < HashWithIndifferentAccess class FieldNotFoundError < RuntimeError; end # Uses #merge! instead of #update when creating a new FieldsHash so #update can update # field values, not the field objects themselves. def initialize(constructor = {}) if constructor.is_a?(Hash) # super() merge!(constructor) else super(constructor) end end # Ignore requests for a proxy def proxy; self end # Allow field values to be merged in from a hash. # Example: # new_book = { # :name => 'Pickaxe', # :category => 'Programming', # :classic => true, # } # form.book.update(new_book) def update(other_hash) other_hash.each_pair { |key, value| self[key].update(value) } self end def [](key) raise(FieldNotFoundError, "Field named '#{key.to_s}' not found in FieldsHash.") unless self.has_key?(key) super end protected def convert_value(value) value.is_a?(Hash) ? FieldsHash.new(value) : value end def method_missing(method, *args) method = method.to_s if method.gsub!(/=$/, '') && self.has_key?(method) self[method].value = *args else self[method].proxy end end end # Gets mixed into field values (strings, arrays) to make them respond to field methods module FieldProxy attr_accessor :field def method_missing(*args) @field.send(*args) end end class Field include TagProxy attr_accessor :value attr_reader :name, :tags def initialize(tags) @tags = tags reset end def tag tags.first end def initial_value if tag['value'] tag['value'] elsif tag.children tag.children.to_s end end # The name for the field (which may have multiple values) # Multiple form elements with the same name are considered only one field. Fields that return # multiple values when submitted are indicated with square brackets at the end of their # name in HTML, but have no such ending internal to this class. def name tag['name'].gsub(/\[\]$/, '') end def reset @value = initial_value end def to_s self.value.to_s end def proxy returning @value do |value| value.extend(FieldProxy) value.field = self end end # Update the value of the field. # This enables updates to be done recursively through FieldsHashes until a form is reached def update(new_value) self.value = new_value end end class Submit < Field; end class CheckBox < Field def initial_value tag['checked'] ? checked_value : unchecked_value end def checked_value @checkbox_tag = tags.detect {|field_tag| field_tag['type'] == 'checkbox' } @checkbox_tag['value'] end def unchecked_value @hidden_tag = tags.detect {|field_tag| field_tag['type'] == 'hidden' } @hidden_tag ? @hidden_tag['value'] : nil end def value=(value) case value when TrueClass, FalseClass @value = value ? checked_value : unchecked_value when checked_value, unchecked_value super else raise "Checkbox value must be one of #{[checked_value, unchecked_value].inspect}." end end def check self.value = checked_value end def uncheck self.value = unchecked_value end end class RadioButtonGroup < Field def initial_value checked_tags = tags.select {|tag| tag['checked'] } # If multiple radio buttons are checked, Firefox uses the last one # If none, the value is undefined and is not submitted checked_tags.any? ? checked_tags.last['value'] : nil end def options tags.collect {|tag| tag['value'] } end def value=(value) if options.include?(value) @value = value else raise "Can't set value '#{value}' for #{self.name} that isn't one of the radio buttons." end end end class Select < Field def initialize(tags) @options = tags.first.select("option").collect {|option_tag| Option.new(self, option_tag) } super end def initial_value selected_options = @options.select(&:initially_selected) case selected_options.size when 1 selected_options.first.value when 0 # If no option is selected, browsers generally use the first @options.first.value else # When multiple options selected but the multiple attribute is not specified, # Firefox selects the last of the options. selected_options.last.value end end def options if options_are_labeled? @options.collect do |option| [option.label, option.value] end else @options.collect(&:value) end end # True if options are like rather than # or def options_are_labeled? @options.any? {|option| option.label } end # If +value+ is a label, return the real value. If not an option, raise error. def lookup_in_options(value) if options.include?(value) return value elsif options_are_labeled? && pair = options.detect {|option| option.include?(value.to_s) } return pair.last else raise "Value '#{value}' isn't one of the options for #{self.name}." end end def value=(value) @value = lookup_in_options(value) end end # A select element that allows multiple values to be set class SelectMultiple < Select class NameMissingSquareBracketsError < RuntimeError; end def initialize(tags) super raise NameMissingSquareBracketsError, "The name of #{name} must be #{name}[] for multiple values to be sent to Rails' params" unless tag['name'] =~ /\[\]$/ end def initial_value @options.select(&:initially_selected).collect(&:value) end def value=(values) @value = values.collect {|value| lookup_in_options(value) } end end class Option attr_reader :tag, :label, :value, :initially_selected def initialize(select, tag) @select, @tag = select, tag @initially_selected = tag['selected'] content = tag.children.to_s value = tag['value'] if value && value != content # Like @label = content @value = value else # Label is nil if like or value == content @value = content end end end class Hidden < Field def value=(value) raise TypeError, "Can't modify hidden field's value" end # Permit changing the value of a hidden field (as if using Javascript) def set_value(value) @value = value end end def select_link(text=nil) @html_document = nil # So it always grabs the latest response if css_select(%Q{a[href="#{text}"]}).any? links = assert_select("a[href=?]", text) elsif text.nil? links = assert_select('a', 1) else links = assert_select('a', text) end decorate_link(links.first) end def decorate_link(link) link.extend FormTestHelper::Link link.testcase = self link end def select_form(text=nil, use_xhr=false) @html_document = nil # So it always grabs the latest response forms = case when text.nil? assert_select("form", 1) when css_select(%Q{form[action="#{text}"]}).any? assert_select("form[action=?]", text) else assert_select('form#?', text) end returning Form.new(forms.first, self) do |form| if block_given? yield form form.submit :xhr => use_xhr end end end # Alias for select_form when called with a block. # Shortcut for select_form(name).submit(args) without block. def submit_form(*args, &block) if block_given? if args[0].is_a?(Hash) select_form(nil, args[0].delete(:xhr), &block) else select_form(*args, &block) end else selector = args[0].is_a?(Hash) ? nil : args.shift select_form(selector).submit(*args) end end module Link def follow path = self.href @testcase.make_request(request_method, path) end alias_method :click, :follow def href self["href"] end def request_method if self["onclick"] && self["onclick"] =~ /'_method'.*'value', '(\w+)'/ $1.to_sym else :get end end def testcase=(testcase) @testcase = testcase self end end def make_request(method, path, params={}, referring_uri=nil, use_xhr=false) if self.kind_of?(ActionController::IntegrationTest) if use_xhr params = {'_method' => method }.merge(params) xml_http_request :post, path, params else self.send(method, path, params.stringify_keys, {:referer => referring_uri}) end else params.merge!(ActionController::Routing::Routes.recognize_path(path, :method => method)) # if params[:controller] && params[:controller] != current_controller = self.instance_eval("@controller").controller_path # raise "Can't follow links outside of current controller (from #{current_controller} to #{params[:controller]})" # end self.instance_eval("@request").env["HTTP_REFERER"] ||= referring_uri # facilitate testing of redirect_to :back if use_xhr self.xhr(method, params.delete(:action), params.stringify_keys) else self.send(method, params.delete(:action), params.stringify_keys) end end end end