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
REMOTE_FORM_ONSUBMIT_ACTION_RGX = /new Ajax.Request\('([^']+)'/
def initialize(tag, testcase, options={})
@tag, @testcase = tag, testcase,
@submit_value = options.delete(:submit_value)
@xhr = options.delete(:xhr)
end
def xhr?
@xhr
end
# If you submit the form with JavaScript
def submit_without_clicking_button
if xhr?
if tag.attributes['onsubmit'] =~ REMOTE_FORM_ONSUBMIT_ACTION_RGX
path = $1
else
raise "No path found for the remote request"
end
else
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
end
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={})
msg = "Submit button not found in form"
selector = 'input[type="submit"], input[type="image"], button[type="submit"]'
if @submit_value
msg << " with a value of '#{@submit_value}'"
selector.gsub!(/\]/, "][value=#{@submit_value}]")
end
raise MissingSubmitError, msg unless tag.select(selector).any?
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
return @fields if @fields
# Input, textarea, select, and button are valid field tags. Name is a required attribute.
fields = tag.select('input, textarea, select, button').reject{ |tag| tag['name'].nil? }
@fields = fields.group_by {|field_tag| field_tag['name'] }.collect do |name, field_tags|
case field_tags.first['type']
when 'submit'
field_tags.reject!{ |tag,*| tag['value'] != @submit_value } if @submit_value
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)
unless self.has_key?(key)
raise(FieldNotFoundError, "Field named '#{key.to_s}' not found in FieldsHash.")
end
super
end
# Allows setting form field values using key access to form fields:
# Examples:
# form = select_form
# form.user['name'] = 'joe'
#
# submit_form do |form|
# form.user['name'] = 'joe'
# end
#
def []=(key, value)
self[key].value = value
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
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
end