require "hpricot" require "English" module ActionController module Integration class Session # Issues a GET request for a page, follows any redirects, and verifies the final page # load was successful. # # Example: # visits "/" def visits(path) request_page(:get, path) end # 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. # # clicks_link has very basic support for detecting Rails-generated # JavaScript onclick handlers for PUT, POST and DELETE links, as well as # CSRF authenticity tokens if they are present. # # Example: # clicks_link "Sign up" def clicks_link(link_text) link = links.detect { |el| el.innerHTML =~ /#{link_text}/i } return flunk("No link with text #{link_text.inspect} was found") if link.nil? onclick = link.attributes["onclick"] href = link.attributes["href"] http_method = http_method_from_js(onclick) authenticity_token = authenticity_token_value(onclick) request_page(http_method, href, authenticity_token.blank? ? {} : {"authenticity_token" => authenticity_token}) end # Works like clicks_link, but forces a GET request # # Example: # clicks_get_link "Log out" def clicks_get_link(link_text) clicks_link_with_method(link_text, :get) end # Works like clicks_link, but issues a DELETE request instead of a GET # # Example: # clicks_delete_link "Log out" def clicks_delete_link(link_text) clicks_link_with_method(link_text, :delete) end # Works like clicks_link, but issues a POST request instead of a GET # # Example: # clicks_post_link "Vote" def clicks_post_link(link_text) clicks_link_with_method(link_text, :post) end # Works like clicks_link, but issues a PUT request instead of a GET # # Example: # clicks_put_link "Update profile" def clicks_put_link(link_text) clicks_link_with_method(link_text, :put) end # 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. # # Examples: # fills_in "Email", :with => "user@example.com" # fills_in "user[email]", :with => "user@example.com" # # The field value is required, and must be specified in options[:with]. # field can be either the value of a name attribute (i.e. user[email]) # or the text inside a element that points at the field. def fills_in(field, options = {}) value = options[:with] return flunk("No value was provided") if value.nil? input = find_field_by_name_or_label(field) return flunk("Could not find input #{field.inspect}") if input.nil? add_form_data(input, value) # TODO - Set current form end # Verifies that a an option element exists on the current page with the specified # text. You can optionally restrict the search to a specific select list by # assigning options[:from] the value of the select list's name or # a label. Stores the option's value to be sent when the form is submitted. # # Examples: # selects "January" # selects "February", :from => "event_month" # selects "February", :from => "Event Month" def selects(option_text, options = {}) if options[:from] select = find_select_list_by_name_or_label(options[:from]) return flunk("Could not find select list #{options[:from].inspect}") if select.nil? option_node = find_option_by_value(option_text, select) return flunk("Could not find option #{option_text.inspect}") if option_node.nil? else option_node = find_option_by_value(option_text) return flunk("Could not find option #{option_text.inspect}") if option_node.nil? select = option_node.parent end add_form_data(select, option_node.attributes["value"] || option_node.innerHTML) # TODO - Set current form end # Verifies that an input checkbox exists on the current page and marks it # as checked, so that the value will be submitted with the form. # # Example: # checks 'Remember Me' def checks(field) checkbox = find_field_by_name_or_label(field) return flunk("Could not find checkbox #{field.inspect}") if checkbox.nil? return flunk("Input #{checkbox.inspect} is not a checkbox") unless checkbox.attributes['type'] == 'checkbox' add_form_data(checkbox, checkbox.attributes["value"] || "on") end # Verifies that an input checkbox exists on the current page and marks it # as unchecked, so that the value will not be submitted with the form. # # Example: # unchecks 'Remember Me' def unchecks(field) checkbox = find_field_by_name_or_label(field) return flunk("Could not find checkbox #{field.inspect}") if checkbox.nil? return flunk("Input #{checkbox.inspect} is not a checkbox") unless checkbox.attributes['type'] == 'checkbox' remove_form_data(checkbox) (form_for_node(checkbox) / "input").each do |input| next unless input.attributes["type"] == "hidden" && input.attributes["name"] == checkbox.attributes["name"] add_form_data(input, input.attributes["value"]) end end # Verifies that a submit button exists for the form, then submits the form, follows # any redirects, and verifies the final page was successful. # # Example: # clicks_button "Login" # clicks_button # # The URL and HTTP method for the form submission are automatically read from the # action and method attributes of the
element. def clicks_button(value = nil) button = value ? find_button(value) : submit_buttons.first return flunk("Could not find button #{value.inspect}") if button.nil? add_form_data(button, button.attributes["value"]) unless button.attributes["name"].blank? submit_form(form_for_node(button)) end def submits_form(form_id = nil) # :nodoc: end protected # Methods you could call, but probably shouldn't def authenticity_token_value(onclick) return unless onclick && onclick.include?("s.setAttribute('name', 'authenticity_token');") && onclick =~ /s\.setAttribute\('value', '([a-f0-9]{40})'\);/ $LAST_MATCH_INFO.captures.first end def http_method_from_js(onclick) if !onclick.blank? && onclick.include?("f.submit()") http_method_from_js_form(onclick) else :get end end def http_method_from_js_form(onclick) if onclick.include?("m.setAttribute('name', '_method')") http_method_from_fake_method_param(onclick) else :post end end def http_method_from_fake_method_param(onclick) if onclick.include?("m.setAttribute('value', 'delete')") :delete elsif onclick.include?("m.setAttribute('value', 'put')") :put else raise "No HTTP method for _method param in #{onclick.inspect}" end end def clicks_link_with_method(link_text, http_method) # :nodoc: link = links.detect { |el| el.innerHTML =~ /#{link_text}/i } return flunk("No link with text #{link_text.inspect} was found") if link.nil? request_page(http_method, link.attributes["href"]) end def find_field_by_name_or_label(name_or_label) # :nodoc: input = find_field_by_name(name_or_label) return input if input label = find_form_label(name_or_label) label ? input_for_label(label) : nil end def find_select_list_by_name_or_label(name_or_label) # :nodoc: select = find_select_list_by_name(name_or_label) return select if select label = find_form_label(name_or_label) label ? select_list_for_label(label) : nil end def find_option_by_value(option_value, select=nil) # :nodoc: options = select.nil? ? option_nodes : (select / "option") options.detect { |el| el.innerHTML == option_value } end def find_button(value = nil) # :nodoc: return nil unless value submit_buttons.detect { |el| el.attributes["value"] == value } end def add_form_data(input_element, value) # :nodoc: form = form_for_node(input_element) data = param_parser.parse_query_parameters("#{input_element.attributes["name"]}=#{value}") merge_form_data(form_number(form), data) end def remove_form_data(input_element) # :nodoc: form = form_for_node(input_element) form_number = form_number(form) form_data[form_number] ||= {} form_data[form_number].delete(input_element.attributes['name']) end def submit_form(form) # :nodoc: form_number = form_number(form) request_page(form_method(form), form_action(form), form_data[form_number]) end def merge_form_data(form_number, data) # :nodoc: form_data[form_number] ||= {} data.each do |key, value| if form_data[form_number][key].is_a?(Hash) merge(form_data[form_number][key], value) else form_data[form_number][key] = value end end end def merge(a, b) # :nodoc: a.keys.each do |k| if b.has_key?(k) and Hash === a[k] and Hash === b[k] a[k] = merge(a[k], b[k]) b.delete(k) end end a.merge!(b) end def request_page(method, url, data = {}) # :nodoc: debug_log "REQUESTING PAGE: #{method.to_s.upcase} #{url} with #{data.inspect}" @current_url = url self.send "#{method}_via_redirect", @current_url, data || {} assert_response :success reset_dom end def input_for_label(label) # :nodoc: if input = (label / "input").first input # nested inputs within labels else # input somewhere else, referenced by id input_id = label.attributes["for"] (dom / "##{input_id}").first end end def select_list_for_label(label) # :nodoc: if select_list = (label / "select").first select_list # nested inputs within labels else # input somewhere else, referenced by id select_list_id = label.attributes["for"] (dom / "##{select_list_id}").first end end def param_parser # :nodoc: if defined?(CGIMethods) CGIMethods else ActionController::AbstractRequest end end def submit_buttons # :nodoc: input_fields.select { |el| el.attributes["type"] == "submit" } end def find_field_by_name(name) # :nodoc: find_input_by_name(name) || find_textarea_by_name(name) end def find_input_by_name(name) # :nodoc: input_fields.detect { |el| el.attributes["name"] == name } end def find_select_list_by_name(name) # :nodoc: select_lists.detect { |el| el.attributes["name"] == name } end def find_textarea_by_name(name) # :nodoc: textarea_fields.detect{ |el| el.attributes['name'] == name } end def find_form_label(text) # :nodoc: candidates = form_labels.select { |el| el.innerText =~ /^\W*#{text}\b/i } candidates.sort_by { |el| el.innerText.strip.size }.first end def form_action(form) # :nodoc: form.attributes["action"].blank? ? current_url : form.attributes["action"] end def form_method(form) # :nodoc: form.attributes["method"].blank? ? :get : form.attributes["method"].downcase end def add_default_params # :nodoc: (dom / "form").each do |form| add_default_params_for(form) end end def add_default_params_for(form) # :nodoc: add_default_params_from_inputs_for(form) add_default_params_from_checkboxes_for(form) add_default_params_from_textateas_for(form) end def add_default_params_from_inputs_for(form) # :nodoc: (form / "input").each do |input| next if input.attributes["value"].blank? || !%w[text hidden].include?(input.attributes["type"]) add_form_data(input, input.attributes["value"]) end end def add_default_params_from_checkboxes_for(form) # :nodoc: (form / "input").each do |input| next if input.attributes["type"] != "checkbox" if input.attributes["checked"] == "checked" add_form_data(input, input.attributes["value"] || "on") end end end def add_default_params_from_textateas_for(form) # :nodoc: (form / "textarea").each do |input| add_form_data(input, input.inner_html) end end def form_for_node(node) # :nodoc: return node if node.name == "form" node = node.parent until node.name == "form" node end def reset_dom # :nodoc: @form_data = [] @dom = nil end def form_data # :nodoc: @form_data ||= [] end def links # :nodoc: (dom / "a[@href]") end def form_number(form) # :nodoc: (dom / "form").index(form) end def input_fields # :nodoc: (dom / "input") end def textarea_fields # :nodoc (dom / "textarea") end def form_labels # :nodoc: (dom / "label") end def select_lists # :nodoc: (dom / "select") end def option_nodes # :nodoc: (dom / "option") end def dom # :nodoc: return @dom if @dom raise "You must visit a path before working with the page." unless response @dom = Hpricot(response.body) add_default_params @dom end def debug_log(message) # :nodoc: return unless logger logger.debug end def logger # :nodoc: if defined? RAILS_DEFAULT_LOGGER RAILS_DEFAULT_LOGGER else nil end end end end end