From a4bef314fe888e3f4df51ad77a22075b5965250e Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 3 Sep 2006 20:22:47 +0000 Subject: Follow proper naming conventions git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4934 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/lib/action_controller/assertions.rb | 12 +- .../lib/action_controller/assertions/assert_dom.rb | 28 -- .../assertions/assert_response.rb | 137 ----- .../action_controller/assertions/assert_routing.rb | 85 ---- .../action_controller/assertions/assert_select.rb | 555 --------------------- .../lib/action_controller/assertions/assert_tag.rb | 119 ----- .../action_controller/assertions/assert_valid.rb | 15 - .../action_controller/assertions/dom_assertions.rb | 28 ++ .../assertions/model_assertions.rb | 15 + .../assertions/response_assertions.rb | 137 +++++ .../assertions/routing_assertions.rb | 85 ++++ .../assertions/selector_assertions.rb | 555 +++++++++++++++++++++ .../action_controller/assertions/tag_assertions.rb | 119 +++++ 13 files changed, 945 insertions(+), 945 deletions(-) delete mode 100644 actionpack/lib/action_controller/assertions/assert_dom.rb delete mode 100644 actionpack/lib/action_controller/assertions/assert_response.rb delete mode 100644 actionpack/lib/action_controller/assertions/assert_routing.rb delete mode 100644 actionpack/lib/action_controller/assertions/assert_select.rb delete mode 100644 actionpack/lib/action_controller/assertions/assert_tag.rb delete mode 100644 actionpack/lib/action_controller/assertions/assert_valid.rb create mode 100644 actionpack/lib/action_controller/assertions/dom_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/model_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/response_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/routing_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/selector_assertions.rb create mode 100644 actionpack/lib/action_controller/assertions/tag_assertions.rb (limited to 'actionpack/lib') diff --git a/actionpack/lib/action_controller/assertions.rb b/actionpack/lib/action_controller/assertions.rb index c11cc004db..b0297b6c01 100644 --- a/actionpack/lib/action_controller/assertions.rb +++ b/actionpack/lib/action_controller/assertions.rb @@ -47,12 +47,12 @@ module ActionController #:nodoc: end end -require File.dirname(__FILE__) + '/assertions/assert_response' -require File.dirname(__FILE__) + '/assertions/assert_select' -require File.dirname(__FILE__) + '/assertions/assert_tag' -require File.dirname(__FILE__) + '/assertions/assert_dom' -require File.dirname(__FILE__) + '/assertions/assert_routing' -require File.dirname(__FILE__) + '/assertions/assert_valid' +require File.dirname(__FILE__) + '/assertions/response_assertions' +require File.dirname(__FILE__) + '/assertions/selector_assertions' +require File.dirname(__FILE__) + '/assertions/tag_assertions' +require File.dirname(__FILE__) + '/assertions/dom_assertions' +require File.dirname(__FILE__) + '/assertions/routing_assertions' +require File.dirname(__FILE__) + '/assertions/model_assertions' module Test #:nodoc: module Unit #:nodoc: diff --git a/actionpack/lib/action_controller/assertions/assert_dom.rb b/actionpack/lib/action_controller/assertions/assert_dom.rb deleted file mode 100644 index a765dff2cc..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_dom.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'test/unit' -require 'test/unit/assertions' - -module ActionController - module Assertions - module DomAssertions - # test 2 html strings to be equivalent, i.e. identical up to reordering of attributes - def assert_dom_equal(expected, actual, message="") - clean_backtrace do - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, " expected to be == to\n.", expected_dom.to_s, actual_dom.to_s) - assert_block(full_message) { expected_dom == actual_dom } - end - end - - # negated form of +assert_dom_equivalent+ - def assert_dom_not_equal(expected, actual, message="") - clean_backtrace do - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - full_message = build_message(message, " expected to be != to\n.", expected_dom.to_s, actual_dom.to_s) - assert_block(full_message) { expected_dom != actual_dom } - end - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/assert_response.rb b/actionpack/lib/action_controller/assertions/assert_response.rb deleted file mode 100644 index 5d5effc493..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_response.rb +++ /dev/null @@ -1,137 +0,0 @@ -require 'test/unit' -require 'test/unit/assertions' -require 'rexml/document' -require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" - -module ActionController - module Assertions - module ResponseAssertions - # Asserts that the response is one of the following types: - # - # * :success: Status code was 200 - # * :redirect: Status code was in the 300-399 range - # * :missing: Status code was 404 - # * :error: Status code was in the 500-599 range - # - # You can also pass an explicit status code number as the type, like assert_response(501) - def assert_response(type, message = nil) - clean_backtrace do - if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") - assert_block("") { true } # to count the assertion - elsif type.is_a?(Fixnum) && @response.response_code == type - assert_block("") { true } # to count the assertion - else - assert_block(build_message(message, "Expected response to be a , but was ", type, @response.response_code)) { false } - end - end - end - - # Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, - # such that assert_redirected_to(:controller => "weblog") will also match the redirection of - # redirect_to(:controller => "weblog", :action => "show") and so on. - def assert_redirected_to(options = {}, message=nil) - clean_backtrace do - assert_response(:redirect, message) - return true if options == @response.redirected_to - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - - begin - url = {} - original = { :expected => options, :actual => @response.redirected_to.is_a?(Symbol) ? @response.redirected_to : @response.redirected_to.dup } - original.each do |key, value| - if value.is_a?(Symbol) - value = @controller.respond_to?(value, true) ? @controller.send(value) : @controller.send("hash_for_#{value}_url") - end - - unless value.is_a?(Hash) - request = case value - when NilClass then nil - when /^\w+:\/\// then recognized_request_for(%r{^(\w+://.*?(/|$|\?))(.*)$} =~ value ? $3 : nil) - else recognized_request_for(value) - end - value = request.path_parameters if request - end - - if value.is_a?(Hash) # stringify 2 levels of hash keys - if name = value.delete(:use_route) - route = ActionController::Routing::Routes.named_routes[name] - value.update(route.parameter_shell) - end - - value.stringify_keys! - value.values.select { |v| v.is_a?(Hash) }.collect { |v| v.stringify_keys! } - if key == :expected && value['controller'] == @controller.controller_name && original[:actual].is_a?(Hash) - original[:actual].stringify_keys! - value.delete('controller') if original[:actual]['controller'].nil? || original[:actual]['controller'] == value['controller'] - end - end - - if value.respond_to?(:[]) && value['controller'] - if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') - value['controller'] = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) - end - value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash - end - url[key] = value - end - - - @response_diff = url[:expected].diff(url[:actual]) if url[:actual] - msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is ), difference: ", - url[:actual], @response_diff) - - assert_block(msg) do - url[:expected].keys.all? do |k| - if k == :controller then url[:expected][k] == ActionController::Routing.controller_relative_to(url[:actual][k], @controller.class.controller_path) - else parameterize(url[:expected][k]) == parameterize(url[:actual][k]) - end - end - end - rescue ActionController::RoutingError # routing failed us, so match the strings only. - msg = build_message(message, "expected a redirect to , found one to ", options, @response.redirect_url) - url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$} - eurl, epath, url, path = [options, @response.redirect_url].collect do |url| - u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url] - [u, (p.first == '/') ? p : '/' + p] - end.flatten - - assert_equal(eurl, url, msg) if eurl && url - assert_equal(epath, path, msg) if epath && path - end - end - end - - # Asserts that the request was rendered with the appropriate template file. - def assert_template(expected = nil, message=nil) - clean_backtrace do - rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file - msg = build_message(message, "expecting but rendering with ", expected, rendered) - assert_block(msg) do - if expected.nil? - !@response.rendered_with_file? - else - expected == rendered - end - end - end - end - - private - def recognized_request_for(path, request_method = nil) - path = "/#{path}" unless path.first == '/' - - # Assume given controller - request = ActionController::TestRequest.new({}, {}, nil) - request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method - request.path = path - - ActionController::Routing::Routes.recognize(request) - request - end - - def parameterize(value) - value.respond_to?(:to_param) ? value.to_param : value - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/assert_routing.rb b/actionpack/lib/action_controller/assertions/assert_routing.rb deleted file mode 100644 index a94bb719f2..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_routing.rb +++ /dev/null @@ -1,85 +0,0 @@ -require 'test/unit' -require 'test/unit/assertions' - -module ActionController - module Assertions - module RoutingAssertions - # Asserts that the routing of the given path was handled correctly and that the parsed options match. - # - # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') - # - # Pass a hash in the second argument to specify the request method. This is useful for routes - # requiring a specific method. - # - # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) - # - def assert_recognizes(expected_options, path, extras={}, message=nil) - if path.is_a? Hash - request_method = path[:method] - path = path[:path] - else - request_method = nil - end - - clean_backtrace do - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - request = recognized_request_for(path, request_method) - - expected_options = expected_options.clone - extras.each_key { |key| expected_options.delete key } unless extras.nil? - - expected_options.stringify_keys! - routing_diff = expected_options.diff(request.path_parameters) - msg = build_message(message, "The recognized options did not match , difference: ", - request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) - assert_block(msg) { request.path_parameters == expected_options } - end - end - - # Asserts that the provided options can be used to generate the provided path. - def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) - clean_backtrace do - expected_path = "/#{expected_path}" unless expected_path[0] == ?/ - # Load routes.rb if it hasn't been loaded. - ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? - - generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, extras) - found_extras = options.reject {|k, v| ! extra_keys.include? k} - - msg = build_message(message, "found extras , not ", found_extras, extras) - assert_block(msg) { found_extras == extras } - - msg = build_message(message, "The generated path did not match ", generated_path, - expected_path) - assert_block(msg) { expected_path == generated_path } - end - end - - # Asserts that path and options match both ways; in other words, the URL generated from - # options is the same as path, and also that the options recognized from path are the same as options - def assert_routing(path, options, defaults={}, extras={}, message=nil) - assert_recognizes(options, path, extras, message) - - controller, default_controller = options[:controller], defaults[:controller] - if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) - options[:controller] = "/#{controller}" - end - - assert_generates(path, options, defaults, extras, message) - end - - private - def recognized_request_for(path, request_method = nil) - path = "/#{path}" unless path.first == '/' - - # Assume given controller - request = ActionController::TestRequest.new({}, {}, nil) - request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method - request.path = path - - ActionController::Routing::Routes.recognize(request) - request - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/assert_select.rb b/actionpack/lib/action_controller/assertions/assert_select.rb deleted file mode 100644 index 7e22d6b655..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_select.rb +++ /dev/null @@ -1,555 +0,0 @@ -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -require 'test/unit' -require 'test/unit/assertions' -require 'rexml/document' -require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" - - -module ActionController - module Assertions - # Adds the #assert_select method for use in Rails functional - # test cases. - # - # Use #assert_select to make assertions on the response HTML of a controller - # action. You can also call #assert_select within another #assert_select to - # make assertions on elements selected by the enclosing assertion. - # - # Use #css_select to select elements without making an assertions, either - # from the response HTML or elements selected by the enclosing assertion. - # - # In addition to HTML responses, you can make the following assertions: - # * #assert_select_rjs -- Assertions on HTML content of RJS update and - # insertion operations. - # * #assert_select_encoded -- Assertions on HTML encoded inside XML, - # for example for dealing with feed item descriptions. - # * #assert_select_email -- Assertions on the HTML body of an e-mail. - # - # Also see HTML::Selector for learning how to use selectors. - module SelectorAssertions - # :call-seq: - # css_select(selector) => array - # css_select(element, selector) => array - # - # Select and return all matching elements. - # - # If called with a single argument, uses that argument as a selector - # to match all elements of the current page. Returns an empty array - # if no match is found. - # - # If called with two arguments, uses the first argument as the base - # element and the second argument as the selector. Attempts to match the - # base element and any of its children. Returns an empty array if no - # match is found. - # - # The selector may be a CSS selector expression (+String+), an expression - # with substitution values (+Array+) or an HTML::Selector object. - # - # For example: - # forms = css_select("form") - # forms.each do |form| - # inputs = css_select(form, "input") - # ... - # end - def css_select(*args) - # See assert_select to understand what's going on here. - arg = args.shift - - if arg.is_a?(HTML::Node) - root = arg - arg = args.shift - elsif arg == nil - raise ArgumentError, "First arugment is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected - matches = [] - @selected.each do |selected| - subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) - subset.each do |match| - matches << match unless matches.any? { |m| m.equal?(match) } - end - end - - return matches - else - root = response_from_page_or_rjs - end - - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - selector.select(root) - end - - # :call-seq: - # assert_select(selector, equality?, message?) - # assert_select(element, selector, equality?, message?) - # - # An assertion that selects elements and makes one or more equality tests. - # - # If the first argument is an element, selects all matching elements - # starting from (and including) that element and all its children in - # depth-first order. - # - # If no element if specified, calling #assert_select will select from the - # response HTML. Calling #assert_select inside an #assert_select block will - # run the assertion for each element selected by the enclosing assertion. - # - # For example: - # assert_select "ol>li" do |elements| - # elements.each do |element| - # assert_select element, "li" - # end - # end - # Or for short: - # assert_select "ol>li" do - # assert_select "li" - # end - # - # The selector may be a CSS selector expression (+String+), an expression - # with substitution values, or an HTML::Selector object. - # - # === Equality Tests - # - # The equality test may be one of the following: - # * nil/true -- Assertion is true if at least one element is - # selected. - # * String -- Assertion is true if the text value of all - # selected elements equals to the string. - # * Regexp -- Assertion is true if the text value of all - # selected elements matches the regular expression. - # * false -- Assertion is true if no element is selected. - # * Integer -- Assertion is true if exactly that number of - # elements are selected. - # * Range -- Assertion is true if the number of selected - # elements fit the range. - # - # To perform more than one equality tests, use a hash the following keys: - # * :text -- Assertion is true if the text value of each - # selected elements equals to the value (+String+ or +Regexp+). - # * :count -- Assertion is true if the number of matched elements - # is equal to the value. - # * :minimum -- Assertion is true if the number of matched - # elements is at least that value. - # * :maximum -- Assertion is true if the number of matched - # elements is at most that value. - # - # If the method is called with a block, once all equality tests are - # evaluated the block is called with an array of all matched elements. - # - # === Examples - # - # # At least one form element - # assert_select "form" - # - # # Form element includes four input fields - # assert_select "form input", 4 - # - # # Page title is "Welcome" - # assert_select "title", "Welcome" - # - # # Page title is "Welcome" and there is only one title element - # assert_select "title", {:count=>1, :text=>"Welcome"}, - # "Wrong title or more than one title element" - # - # # Page contains no forms - # assert_select "form", false, "This page must contain no forms" - # - # # Test the content and style - # assert_select "body div.header ul.menu" - # - # # Use substitution values - # assert_select "ol>li#?", /item-\d+/ - # - # # All input fields in the form have a name - # assert_select "form input" do - # assert_select "[name=?]", /.+/ # Not empty - # end - def assert_select(*args, &block) - # Start with optional element followed by mandatory selector. - arg = args.shift - - if arg.is_a?(HTML::Node) - # First argument is a node (tag or text, but also HTML root), - # so we know what we're selecting from. - root = arg - arg = args.shift - elsif arg == nil - # This usually happens when passing a node/element that - # happens to be nil. - raise ArgumentError, "First arugment is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected - root = HTML::Node.new(nil) - root.children.concat @selected - else - # Otherwise just operate on the response document. - root = response_from_page_or_rjs - end - - # First or second argument is the selector: string and we pass - # all remaining arguments. Array and we pass the argument. Also - # accepts selector itself. - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - # Next argument is used for equality tests. - equals = {} - case arg = args.shift - when Hash - equals = arg - when String, Regexp - equals[:text] = arg - when Integer - equals[:count] = arg - when Range - equals[:minimum] = arg.begin - equals[:maximum] = arg.end - when FalseClass - equals[:count] = 0 - when NilClass, TrueClass - equals[:minimum] = 1 - else raise ArgumentError, "I don't understand what you're trying to match" - end - - # If we have a text test, by default we're looking for at least one match. - # Without this statement text tests pass even if nothing is selected. - # Can always override by specifying minimum or count. - if equals[:text] - equals[:minimum] ||= 1 - end - # If a count is specified, it takes precedence over minimum/maximum. - if equals[:count] - equals[:minimum] = equals[:maximum] = equals.delete(:count) - end - - # Last argument is the message we use if the assertion fails. - message = args.shift - #- message = "No match made with selector #{selector.inspect}" unless message - if args.shift - raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" - end - - matches = selector.select(root) - # Equality test. - equals.each do |type, value| - case type - when :text - for match in matches - text = "" - stack = match.children.reverse - while node = stack.pop - if node.tag? - stack.concat node.children.reverse - else - text << node.content - end - end - text.strip! unless match.name == "pre" - if value.is_a?(Regexp) - assert text =~ value, build_message(message, < expected but was -. -EOT - else - assert_equal value.to_s, text, message - end - end - when :html - for match in matches - html = match.children.map(&:to_s).join - html.strip! unless match.name == "pre" - if value.is_a?(Regexp) - assert html =~ value, build_message(message, < expected but was -. -EOT - else - assert_equal value.to_s, html, message - end - end - when :minimum - assert matches.size >= value, message || "Expecting at least #{value} selected elements, found #{matches.size}" - when :maximum - assert matches.size <= value, message || "Expecting at most #{value} selected elements, found #{matches.size}" - else raise ArgumentError, "I don't support the equality test #{key}" - end - end - - # If a block is given call that block. Set @selected to allow - # nested assert_select, which can be nested several levels deep. - if block_given? && !matches.empty? - begin - in_scope, @selected = @selected, matches - yield matches - ensure - @selected = in_scope - end - end - - # Returns all matches elements. - matches - end - - # :call-seq: - # assert_select_rjs(id?) { |elements| ... } - # assert_select_rjs(statement, id?) { |elements| ... } - # assert_select_rjs(:insert, position, id?) { |elements| ... } - # - # Selects content from the RJS response. - # - # === Narrowing down - # - # With no arguments, asserts that one or more elements are updated or - # inserted by RJS statements. - # - # Use the +id+ argument to narrow down the assertion to only statements - # that update or insert an element with that identifier. - # - # Use the first argument to narrow down assertions to only statements - # of that type. Possible values are +:replace+, +:replace_html+ and - # +:insert_html+. - # - # Use the argument +:insert+ followed by an insertion position to narrow - # down the assertion to only statements that insert elements in that - # position. Possible values are +:top+, +:bottom+, +:before+ and +:after+. - # - # === Using blocks - # - # Without a block, #assert_select_rjs merely asserts that the response - # contains one or more RJS statements that replace or update content. - # - # With a block, #assert_select_rjs also selects all elements used in - # these statements and passes them to the block. Nested assertions are - # supported. - # - # Calling #assert_select_rjs with no arguments and using nested asserts - # asserts that the HTML content is returned by one or more RJS statements. - # Using #assert_select directly makes the same assertion on the content, - # but without distinguishing whether the content is returned in an HTML - # or JavaScript. - # - # === Examples - # - # # Updating the element foo. - # assert_select_rjs :update, "foo" - # - # # Inserting into the element bar, top position. - # assert_select rjs, :insert, :top, "bar" - # - # # Changing the element foo, with an image. - # assert_select_rjs "foo" do - # assert_select "img[src=/images/logo.gif"" - # end - # - # # RJS inserts or updates a list with four items. - # assert_select_rjs do - # assert_select "ol>li", 4 - # end - # - # # The same, but shorter. - # assert_select "ol>li", 4 - def assert_select_rjs(*args, &block) - arg = args.shift - - # If the first argument is a symbol, it's the type of RJS statement we're looking - # for (update, replace, insertion, etc). Otherwise, we're looking for just about - # any RJS statement. - if arg.is_a?(Symbol) - if arg == :insert - arg = args.shift - insertion = "insert_#{arg}".to_sym - raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] - statement = "(#{RJS_STATEMENTS[insertion]})" - else - raise ArgumentError, "Unknown RJS statement type #{arg}" unless RJS_STATEMENTS[arg] - statement = "(#{RJS_STATEMENTS[arg]})" - end - arg = args.shift - else - statement = "#{RJS_STATEMENTS[:any]}" - end - - # Next argument we're looking for is the element identifier. If missing, we pick - # any element. - if arg.is_a?(String) - id = Regexp.quote(arg) - arg = args.shift - else - id = "[^\"]*" - end - - pattern = Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) - - # Duplicate the body since the next step involves destroying it. - matches = nil - @response.body.gsub(pattern) do |match| - html = $2 - # RJS encodes double quotes and line breaks. - html.gsub!(/\\"/, "\"") - html.gsub!(/\\n/, "\n") - matches ||= [] - matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } - "" - end - if matches - if block_given? - begin - in_scope, @selected = @selected, matches - yield matches - ensure - @selected = in_scope - end - end - matches - else - # RJS statement not found. - flunk args.shift || "No RJS statement that replaces or inserts HTML content." - end - end - - # :call-seq: - # assert_select_encoded(element?) { |elements| ... } - # - # Extracts the content of an element, treats it as encoded HTML and runs - # nested assertion on it. - # - # You typically call this method within another assertion to operate on - # all currently selected elements. You can also pass an element or array - # of elements. - # - # The content of each element is un-encoded, and wrapped in the root - # element +encoded+. It then calls the block with all un-encoded elements. - # - # === Example - # - # assert_select_feed :rss, 2.0 do - # # Select description element of each feed item. - # assert_select "channel>item>description" do - # # Run assertions on the encoded elements. - # assert_select_encoded do - # assert_select "p" - # end - # end - # end - def assert_select_encoded(element = nil, &block) - case element - when Array - elements = element - when HTML::Node - elements = [element] - when nil - unless elements = @selected - raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" - end - else - raise ArgumentError, "Argument is optional, and may be node or array of nodes" - end - - fix_content = lambda do |node| - # Gets around a bug in the Rails 1.1 HTML parser. - node.content.gsub(/)?/m) { CGI.escapeHTML($1) } - end - - selected = elements.map do |element| - text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join - root = HTML::Document.new(CGI.unescapeHTML("#{text}")).root - css_select(root, "encoded:root", &block)[0] - end - - begin - old_selected, @selected = @selected, selected - assert_select ":root", &block - ensure - @selected = old_selected - end - end - - # :call-seq: - # assert_select_email { } - # - # Extracts the body of an email and runs nested assertions on it. - # - # You must enable deliveries for this assertion to work, use: - # ActionMailer::Base.perform_deliveries = true - # - # === Example - # - # assert_select_email do - # assert_select "h1", "Email alert" - # end - def assert_select_email(&block) - deliveries = ActionMailer::Base.deliveries - assert !deliveries.empty?, "No e-mail in delivery list" - - for delivery in deliveries - for part in delivery.parts - if part["Content-Type"].to_s =~ /^text\/html\W/ - root = HTML::Document.new(part.body).root - assert_select root, ":root", &block - end - end - end - end - - protected - unless const_defined?(:RJS_STATEMENTS) - RJS_STATEMENTS = { - :replace => /Element\.replace/, - :replace_html => /Element\.update/ - } - RJS_INSERTIONS = [:top, :bottom, :before, :after] - RJS_INSERTIONS.each do |insertion| - RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}")) - end - RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") - RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion| - Regexp.quote("new Insertion.#{insertion.to_s.camelize}") - end.join('|')) - RJS_PATTERN_HTML = /"((\\"|[^"])*)"/ - RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", - Regexp::MULTILINE) - end - - # #assert_select and #css_select call this to obtain the content in the HTML - # page, or from all the RJS statements, depending on the type of response. - def response_from_page_or_rjs() - content_type = @response.headers["Content-Type"] - if content_type && content_type =~ /text\/javascript/ - body = @response.body.dup - root = HTML::Node.new(nil) - while true - next if body.sub!(RJS_PATTERN_EVERYTHING) do |match| - # RJS encodes double quotes and line breaks. - html = $3 - html.gsub!(/\\"/, "\"") - html.gsub!(/\\n/, "\n") - matches = HTML::Document.new(html).root.children.select { |n| n.tag? } - root.children.concat matches - "" - end - break - end - root - else - html_document.root - end - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/assert_tag.rb b/actionpack/lib/action_controller/assertions/assert_tag.rb deleted file mode 100644 index dc80b442b7..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_tag.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'test/unit' -require 'test/unit/assertions' -require 'rexml/document' -require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" - -module ActionController - module Assertions - module TagAssertions - # Asserts that there is a tag/node/element in the body of the response - # that meets all of the given conditions. The +conditions+ parameter must - # be a hash of any of the following keys (all are optional): - # - # * :tag: the node type must match the corresponding value - # * :attributes: a hash. The node's attributes must match the - # corresponding values in the hash. - # * :parent: a hash. The node's parent must match the - # corresponding hash. - # * :child: a hash. At least one of the node's immediate children - # must meet the criteria described by the hash. - # * :ancestor: a hash. At least one of the node's ancestors must - # meet the criteria described by the hash. - # * :descendant: a hash. At least one of the node's descendants - # must meet the criteria described by the hash. - # * :sibling: a hash. At least one of the node's siblings must - # meet the criteria described by the hash. - # * :after: a hash. The node must be after any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * :before: a hash. The node must be before any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * :children: a hash, for counting children of a node. Accepts - # the keys: - # * :count: either a number or a range which must equal (or - # include) the number of children that match. - # * :less_than: the number of matching children must be less - # than this number. - # * :greater_than: the number of matching children must be - # greater than this number. - # * :only: another hash consisting of the keys to use - # to match on the children, and only matching children will be - # counted. - # * :content: the textual content of the node must match the - # given value. This will not match HTML tags in the body of a - # tag--only text. - # - # Conditions are matched using the following algorithm: - # - # * if the condition is a string, it must be a substring of the value. - # * if the condition is a regexp, it must match the value. - # * if the condition is a number, the value must match number.to_s. - # * if the condition is +true+, the value must not be +nil+. - # * if the condition is +false+ or +nil+, the value must be +nil+. - # - # Usage: - # - # # assert that there is a "span" tag - # assert_tag :tag => "span" - # - # # assert that there is a "span" tag with id="x" - # assert_tag :tag => "span", :attributes => { :id => "x" } - # - # # assert that there is a "span" tag using the short-hand - # assert_tag :span - # - # # assert that there is a "span" tag with id="x" using the short-hand - # assert_tag :span, :attributes => { :id => "x" } - # - # # assert that there is a "span" inside of a "div" - # assert_tag :tag => "span", :parent => { :tag => "div" } - # - # # assert that there is a "span" somewhere inside a table - # assert_tag :tag => "span", :ancestor => { :tag => "table" } - # - # # assert that there is a "span" with at least one "em" child - # assert_tag :tag => "span", :child => { :tag => "em" } - # - # # assert that there is a "span" containing a (possibly nested) - # # "strong" tag. - # assert_tag :tag => "span", :descendant => { :tag => "strong" } - # - # # assert that there is a "span" containing between 2 and 4 "em" tags - # # as immediate children - # assert_tag :tag => "span", - # :children => { :count => 2..4, :only => { :tag => "em" } } - # - # # get funky: assert that there is a "div", with an "ul" ancestor - # # and an "li" parent (with "class" = "enum"), and containing a - # # "span" descendant that contains text matching /hello world/ - # assert_tag :tag => "div", - # :ancestor => { :tag => "ul" }, - # :parent => { :tag => "li", - # :attributes => { :class => "enum" } }, - # :descendant => { :tag => "span", - # :child => /hello world/ } - # - # Please noteYou must explicitly - # close all of your tags to use these assertions. - def assert_tag(*opts) - clean_backtrace do - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - end - - # Identical to #assert_tag, but asserts that a matching tag does _not_ - # exist. (See #assert_tag for a full discussion of the syntax.) - def assert_no_tag(*opts) - clean_backtrace do - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/assert_valid.rb b/actionpack/lib/action_controller/assertions/assert_valid.rb deleted file mode 100644 index b807a8902f..0000000000 --- a/actionpack/lib/action_controller/assertions/assert_valid.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'test/unit' -require 'test/unit/assertions' - -module ActionController - module Assertions - module ModelAssertions - # ensures that the passed record is valid by active record standards. returns the error messages if not - def assert_valid(record) - clean_backtrace do - assert record.valid?, record.errors.full_messages.join("\n") - end - end - end - end -end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/dom_assertions.rb b/actionpack/lib/action_controller/assertions/dom_assertions.rb new file mode 100644 index 0000000000..a765dff2cc --- /dev/null +++ b/actionpack/lib/action_controller/assertions/dom_assertions.rb @@ -0,0 +1,28 @@ +require 'test/unit' +require 'test/unit/assertions' + +module ActionController + module Assertions + module DomAssertions + # test 2 html strings to be equivalent, i.e. identical up to reordering of attributes + def assert_dom_equal(expected, actual, message="") + clean_backtrace do + expected_dom = HTML::Document.new(expected).root + actual_dom = HTML::Document.new(actual).root + full_message = build_message(message, " expected to be == to\n.", expected_dom.to_s, actual_dom.to_s) + assert_block(full_message) { expected_dom == actual_dom } + end + end + + # negated form of +assert_dom_equivalent+ + def assert_dom_not_equal(expected, actual, message="") + clean_backtrace do + expected_dom = HTML::Document.new(expected).root + actual_dom = HTML::Document.new(actual).root + full_message = build_message(message, " expected to be != to\n.", expected_dom.to_s, actual_dom.to_s) + assert_block(full_message) { expected_dom != actual_dom } + end + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/model_assertions.rb b/actionpack/lib/action_controller/assertions/model_assertions.rb new file mode 100644 index 0000000000..b807a8902f --- /dev/null +++ b/actionpack/lib/action_controller/assertions/model_assertions.rb @@ -0,0 +1,15 @@ +require 'test/unit' +require 'test/unit/assertions' + +module ActionController + module Assertions + module ModelAssertions + # ensures that the passed record is valid by active record standards. returns the error messages if not + def assert_valid(record) + clean_backtrace do + assert record.valid?, record.errors.full_messages.join("\n") + end + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb new file mode 100644 index 0000000000..5d5effc493 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/response_assertions.rb @@ -0,0 +1,137 @@ +require 'test/unit' +require 'test/unit/assertions' +require 'rexml/document' +require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" + +module ActionController + module Assertions + module ResponseAssertions + # Asserts that the response is one of the following types: + # + # * :success: Status code was 200 + # * :redirect: Status code was in the 300-399 range + # * :missing: Status code was 404 + # * :error: Status code was in the 500-599 range + # + # You can also pass an explicit status code number as the type, like assert_response(501) + def assert_response(type, message = nil) + clean_backtrace do + if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") + assert_block("") { true } # to count the assertion + elsif type.is_a?(Fixnum) && @response.response_code == type + assert_block("") { true } # to count the assertion + else + assert_block(build_message(message, "Expected response to be a , but was ", type, @response.response_code)) { false } + end + end + end + + # Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, + # such that assert_redirected_to(:controller => "weblog") will also match the redirection of + # redirect_to(:controller => "weblog", :action => "show") and so on. + def assert_redirected_to(options = {}, message=nil) + clean_backtrace do + assert_response(:redirect, message) + return true if options == @response.redirected_to + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + + begin + url = {} + original = { :expected => options, :actual => @response.redirected_to.is_a?(Symbol) ? @response.redirected_to : @response.redirected_to.dup } + original.each do |key, value| + if value.is_a?(Symbol) + value = @controller.respond_to?(value, true) ? @controller.send(value) : @controller.send("hash_for_#{value}_url") + end + + unless value.is_a?(Hash) + request = case value + when NilClass then nil + when /^\w+:\/\// then recognized_request_for(%r{^(\w+://.*?(/|$|\?))(.*)$} =~ value ? $3 : nil) + else recognized_request_for(value) + end + value = request.path_parameters if request + end + + if value.is_a?(Hash) # stringify 2 levels of hash keys + if name = value.delete(:use_route) + route = ActionController::Routing::Routes.named_routes[name] + value.update(route.parameter_shell) + end + + value.stringify_keys! + value.values.select { |v| v.is_a?(Hash) }.collect { |v| v.stringify_keys! } + if key == :expected && value['controller'] == @controller.controller_name && original[:actual].is_a?(Hash) + original[:actual].stringify_keys! + value.delete('controller') if original[:actual]['controller'].nil? || original[:actual]['controller'] == value['controller'] + end + end + + if value.respond_to?(:[]) && value['controller'] + if key == :actual && value['controller'].first != '/' && !value['controller'].include?('/') + value['controller'] = ActionController::Routing.controller_relative_to(value['controller'], @controller.class.controller_path) + end + value['controller'] = value['controller'][1..-1] if value['controller'].first == '/' # strip leading hash + end + url[key] = value + end + + + @response_diff = url[:expected].diff(url[:actual]) if url[:actual] + msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is ), difference: ", + url[:actual], @response_diff) + + assert_block(msg) do + url[:expected].keys.all? do |k| + if k == :controller then url[:expected][k] == ActionController::Routing.controller_relative_to(url[:actual][k], @controller.class.controller_path) + else parameterize(url[:expected][k]) == parameterize(url[:actual][k]) + end + end + end + rescue ActionController::RoutingError # routing failed us, so match the strings only. + msg = build_message(message, "expected a redirect to , found one to ", options, @response.redirect_url) + url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$} + eurl, epath, url, path = [options, @response.redirect_url].collect do |url| + u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url] + [u, (p.first == '/') ? p : '/' + p] + end.flatten + + assert_equal(eurl, url, msg) if eurl && url + assert_equal(epath, path, msg) if epath && path + end + end + end + + # Asserts that the request was rendered with the appropriate template file. + def assert_template(expected = nil, message=nil) + clean_backtrace do + rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file + msg = build_message(message, "expecting but rendering with ", expected, rendered) + assert_block(msg) do + if expected.nil? + !@response.rendered_with_file? + else + expected == rendered + end + end + end + end + + private + def recognized_request_for(path, request_method = nil) + path = "/#{path}" unless path.first == '/' + + # Assume given controller + request = ActionController::TestRequest.new({}, {}, nil) + request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method + request.path = path + + ActionController::Routing::Routes.recognize(request) + request + end + + def parameterize(value) + value.respond_to?(:to_param) ? value.to_param : value + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/routing_assertions.rb b/actionpack/lib/action_controller/assertions/routing_assertions.rb new file mode 100644 index 0000000000..a94bb719f2 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/routing_assertions.rb @@ -0,0 +1,85 @@ +require 'test/unit' +require 'test/unit/assertions' + +module ActionController + module Assertions + module RoutingAssertions + # Asserts that the routing of the given path was handled correctly and that the parsed options match. + # + # assert_recognizes({:controller => 'items', :action => 'index'}, 'items') + # + # Pass a hash in the second argument to specify the request method. This is useful for routes + # requiring a specific method. + # + # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post}) + # + def assert_recognizes(expected_options, path, extras={}, message=nil) + if path.is_a? Hash + request_method = path[:method] + path = path[:path] + else + request_method = nil + end + + clean_backtrace do + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + request = recognized_request_for(path, request_method) + + expected_options = expected_options.clone + extras.each_key { |key| expected_options.delete key } unless extras.nil? + + expected_options.stringify_keys! + routing_diff = expected_options.diff(request.path_parameters) + msg = build_message(message, "The recognized options did not match , difference: ", + request.path_parameters, expected_options, expected_options.diff(request.path_parameters)) + assert_block(msg) { request.path_parameters == expected_options } + end + end + + # Asserts that the provided options can be used to generate the provided path. + def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) + clean_backtrace do + expected_path = "/#{expected_path}" unless expected_path[0] == ?/ + # Load routes.rb if it hasn't been loaded. + ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? + + generated_path, extra_keys = ActionController::Routing::Routes.generate_extras(options, extras) + found_extras = options.reject {|k, v| ! extra_keys.include? k} + + msg = build_message(message, "found extras , not ", found_extras, extras) + assert_block(msg) { found_extras == extras } + + msg = build_message(message, "The generated path did not match ", generated_path, + expected_path) + assert_block(msg) { expected_path == generated_path } + end + end + + # Asserts that path and options match both ways; in other words, the URL generated from + # options is the same as path, and also that the options recognized from path are the same as options + def assert_routing(path, options, defaults={}, extras={}, message=nil) + assert_recognizes(options, path, extras, message) + + controller, default_controller = options[:controller], defaults[:controller] + if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) + options[:controller] = "/#{controller}" + end + + assert_generates(path, options, defaults, extras, message) + end + + private + def recognized_request_for(path, request_method = nil) + path = "/#{path}" unless path.first == '/' + + # Assume given controller + request = ActionController::TestRequest.new({}, {}, nil) + request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method + request.path = path + + ActionController::Routing::Routes.recognize(request) + request + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb new file mode 100644 index 0000000000..7e22d6b655 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb @@ -0,0 +1,555 @@ +#-- +# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) +# Under MIT and/or CC By license. +#++ + +require 'test/unit' +require 'test/unit/assertions' +require 'rexml/document' +require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" + + +module ActionController + module Assertions + # Adds the #assert_select method for use in Rails functional + # test cases. + # + # Use #assert_select to make assertions on the response HTML of a controller + # action. You can also call #assert_select within another #assert_select to + # make assertions on elements selected by the enclosing assertion. + # + # Use #css_select to select elements without making an assertions, either + # from the response HTML or elements selected by the enclosing assertion. + # + # In addition to HTML responses, you can make the following assertions: + # * #assert_select_rjs -- Assertions on HTML content of RJS update and + # insertion operations. + # * #assert_select_encoded -- Assertions on HTML encoded inside XML, + # for example for dealing with feed item descriptions. + # * #assert_select_email -- Assertions on the HTML body of an e-mail. + # + # Also see HTML::Selector for learning how to use selectors. + module SelectorAssertions + # :call-seq: + # css_select(selector) => array + # css_select(element, selector) => array + # + # Select and return all matching elements. + # + # If called with a single argument, uses that argument as a selector + # to match all elements of the current page. Returns an empty array + # if no match is found. + # + # If called with two arguments, uses the first argument as the base + # element and the second argument as the selector. Attempts to match the + # base element and any of its children. Returns an empty array if no + # match is found. + # + # The selector may be a CSS selector expression (+String+), an expression + # with substitution values (+Array+) or an HTML::Selector object. + # + # For example: + # forms = css_select("form") + # forms.each do |form| + # inputs = css_select(form, "input") + # ... + # end + def css_select(*args) + # See assert_select to understand what's going on here. + arg = args.shift + + if arg.is_a?(HTML::Node) + root = arg + arg = args.shift + elsif arg == nil + raise ArgumentError, "First arugment is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + matches = [] + @selected.each do |selected| + subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) + subset.each do |match| + matches << match unless matches.any? { |m| m.equal?(match) } + end + end + + return matches + else + root = response_from_page_or_rjs + end + + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + selector.select(root) + end + + # :call-seq: + # assert_select(selector, equality?, message?) + # assert_select(element, selector, equality?, message?) + # + # An assertion that selects elements and makes one or more equality tests. + # + # If the first argument is an element, selects all matching elements + # starting from (and including) that element and all its children in + # depth-first order. + # + # If no element if specified, calling #assert_select will select from the + # response HTML. Calling #assert_select inside an #assert_select block will + # run the assertion for each element selected by the enclosing assertion. + # + # For example: + # assert_select "ol>li" do |elements| + # elements.each do |element| + # assert_select element, "li" + # end + # end + # Or for short: + # assert_select "ol>li" do + # assert_select "li" + # end + # + # The selector may be a CSS selector expression (+String+), an expression + # with substitution values, or an HTML::Selector object. + # + # === Equality Tests + # + # The equality test may be one of the following: + # * nil/true -- Assertion is true if at least one element is + # selected. + # * String -- Assertion is true if the text value of all + # selected elements equals to the string. + # * Regexp -- Assertion is true if the text value of all + # selected elements matches the regular expression. + # * false -- Assertion is true if no element is selected. + # * Integer -- Assertion is true if exactly that number of + # elements are selected. + # * Range -- Assertion is true if the number of selected + # elements fit the range. + # + # To perform more than one equality tests, use a hash the following keys: + # * :text -- Assertion is true if the text value of each + # selected elements equals to the value (+String+ or +Regexp+). + # * :count -- Assertion is true if the number of matched elements + # is equal to the value. + # * :minimum -- Assertion is true if the number of matched + # elements is at least that value. + # * :maximum -- Assertion is true if the number of matched + # elements is at most that value. + # + # If the method is called with a block, once all equality tests are + # evaluated the block is called with an array of all matched elements. + # + # === Examples + # + # # At least one form element + # assert_select "form" + # + # # Form element includes four input fields + # assert_select "form input", 4 + # + # # Page title is "Welcome" + # assert_select "title", "Welcome" + # + # # Page title is "Welcome" and there is only one title element + # assert_select "title", {:count=>1, :text=>"Welcome"}, + # "Wrong title or more than one title element" + # + # # Page contains no forms + # assert_select "form", false, "This page must contain no forms" + # + # # Test the content and style + # assert_select "body div.header ul.menu" + # + # # Use substitution values + # assert_select "ol>li#?", /item-\d+/ + # + # # All input fields in the form have a name + # assert_select "form input" do + # assert_select "[name=?]", /.+/ # Not empty + # end + def assert_select(*args, &block) + # Start with optional element followed by mandatory selector. + arg = args.shift + + if arg.is_a?(HTML::Node) + # First argument is a node (tag or text, but also HTML root), + # so we know what we're selecting from. + root = arg + arg = args.shift + elsif arg == nil + # This usually happens when passing a node/element that + # happens to be nil. + raise ArgumentError, "First arugment is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" + elsif @selected + root = HTML::Node.new(nil) + root.children.concat @selected + else + # Otherwise just operate on the response document. + root = response_from_page_or_rjs + end + + # First or second argument is the selector: string and we pass + # all remaining arguments. Array and we pass the argument. Also + # accepts selector itself. + case arg + when String + selector = HTML::Selector.new(arg, args) + when Array + selector = HTML::Selector.new(*arg) + when HTML::Selector + selector = arg + else raise ArgumentError, "Expecting a selector as the first argument" + end + + # Next argument is used for equality tests. + equals = {} + case arg = args.shift + when Hash + equals = arg + when String, Regexp + equals[:text] = arg + when Integer + equals[:count] = arg + when Range + equals[:minimum] = arg.begin + equals[:maximum] = arg.end + when FalseClass + equals[:count] = 0 + when NilClass, TrueClass + equals[:minimum] = 1 + else raise ArgumentError, "I don't understand what you're trying to match" + end + + # If we have a text test, by default we're looking for at least one match. + # Without this statement text tests pass even if nothing is selected. + # Can always override by specifying minimum or count. + if equals[:text] + equals[:minimum] ||= 1 + end + # If a count is specified, it takes precedence over minimum/maximum. + if equals[:count] + equals[:minimum] = equals[:maximum] = equals.delete(:count) + end + + # Last argument is the message we use if the assertion fails. + message = args.shift + #- message = "No match made with selector #{selector.inspect}" unless message + if args.shift + raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" + end + + matches = selector.select(root) + # Equality test. + equals.each do |type, value| + case type + when :text + for match in matches + text = "" + stack = match.children.reverse + while node = stack.pop + if node.tag? + stack.concat node.children.reverse + else + text << node.content + end + end + text.strip! unless match.name == "pre" + if value.is_a?(Regexp) + assert text =~ value, build_message(message, < expected but was +. +EOT + else + assert_equal value.to_s, text, message + end + end + when :html + for match in matches + html = match.children.map(&:to_s).join + html.strip! unless match.name == "pre" + if value.is_a?(Regexp) + assert html =~ value, build_message(message, < expected but was +. +EOT + else + assert_equal value.to_s, html, message + end + end + when :minimum + assert matches.size >= value, message || "Expecting at least #{value} selected elements, found #{matches.size}" + when :maximum + assert matches.size <= value, message || "Expecting at most #{value} selected elements, found #{matches.size}" + else raise ArgumentError, "I don't support the equality test #{key}" + end + end + + # If a block is given call that block. Set @selected to allow + # nested assert_select, which can be nested several levels deep. + if block_given? && !matches.empty? + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + + # Returns all matches elements. + matches + end + + # :call-seq: + # assert_select_rjs(id?) { |elements| ... } + # assert_select_rjs(statement, id?) { |elements| ... } + # assert_select_rjs(:insert, position, id?) { |elements| ... } + # + # Selects content from the RJS response. + # + # === Narrowing down + # + # With no arguments, asserts that one or more elements are updated or + # inserted by RJS statements. + # + # Use the +id+ argument to narrow down the assertion to only statements + # that update or insert an element with that identifier. + # + # Use the first argument to narrow down assertions to only statements + # of that type. Possible values are +:replace+, +:replace_html+ and + # +:insert_html+. + # + # Use the argument +:insert+ followed by an insertion position to narrow + # down the assertion to only statements that insert elements in that + # position. Possible values are +:top+, +:bottom+, +:before+ and +:after+. + # + # === Using blocks + # + # Without a block, #assert_select_rjs merely asserts that the response + # contains one or more RJS statements that replace or update content. + # + # With a block, #assert_select_rjs also selects all elements used in + # these statements and passes them to the block. Nested assertions are + # supported. + # + # Calling #assert_select_rjs with no arguments and using nested asserts + # asserts that the HTML content is returned by one or more RJS statements. + # Using #assert_select directly makes the same assertion on the content, + # but without distinguishing whether the content is returned in an HTML + # or JavaScript. + # + # === Examples + # + # # Updating the element foo. + # assert_select_rjs :update, "foo" + # + # # Inserting into the element bar, top position. + # assert_select rjs, :insert, :top, "bar" + # + # # Changing the element foo, with an image. + # assert_select_rjs "foo" do + # assert_select "img[src=/images/logo.gif"" + # end + # + # # RJS inserts or updates a list with four items. + # assert_select_rjs do + # assert_select "ol>li", 4 + # end + # + # # The same, but shorter. + # assert_select "ol>li", 4 + def assert_select_rjs(*args, &block) + arg = args.shift + + # If the first argument is a symbol, it's the type of RJS statement we're looking + # for (update, replace, insertion, etc). Otherwise, we're looking for just about + # any RJS statement. + if arg.is_a?(Symbol) + if arg == :insert + arg = args.shift + insertion = "insert_#{arg}".to_sym + raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] + statement = "(#{RJS_STATEMENTS[insertion]})" + else + raise ArgumentError, "Unknown RJS statement type #{arg}" unless RJS_STATEMENTS[arg] + statement = "(#{RJS_STATEMENTS[arg]})" + end + arg = args.shift + else + statement = "#{RJS_STATEMENTS[:any]}" + end + + # Next argument we're looking for is the element identifier. If missing, we pick + # any element. + if arg.is_a?(String) + id = Regexp.quote(arg) + arg = args.shift + else + id = "[^\"]*" + end + + pattern = Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) + + # Duplicate the body since the next step involves destroying it. + matches = nil + @response.body.gsub(pattern) do |match| + html = $2 + # RJS encodes double quotes and line breaks. + html.gsub!(/\\"/, "\"") + html.gsub!(/\\n/, "\n") + matches ||= [] + matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } + "" + end + if matches + if block_given? + begin + in_scope, @selected = @selected, matches + yield matches + ensure + @selected = in_scope + end + end + matches + else + # RJS statement not found. + flunk args.shift || "No RJS statement that replaces or inserts HTML content." + end + end + + # :call-seq: + # assert_select_encoded(element?) { |elements| ... } + # + # Extracts the content of an element, treats it as encoded HTML and runs + # nested assertion on it. + # + # You typically call this method within another assertion to operate on + # all currently selected elements. You can also pass an element or array + # of elements. + # + # The content of each element is un-encoded, and wrapped in the root + # element +encoded+. It then calls the block with all un-encoded elements. + # + # === Example + # + # assert_select_feed :rss, 2.0 do + # # Select description element of each feed item. + # assert_select "channel>item>description" do + # # Run assertions on the encoded elements. + # assert_select_encoded do + # assert_select "p" + # end + # end + # end + def assert_select_encoded(element = nil, &block) + case element + when Array + elements = element + when HTML::Node + elements = [element] + when nil + unless elements = @selected + raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" + end + else + raise ArgumentError, "Argument is optional, and may be node or array of nodes" + end + + fix_content = lambda do |node| + # Gets around a bug in the Rails 1.1 HTML parser. + node.content.gsub(/)?/m) { CGI.escapeHTML($1) } + end + + selected = elements.map do |element| + text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + root = HTML::Document.new(CGI.unescapeHTML("#{text}")).root + css_select(root, "encoded:root", &block)[0] + end + + begin + old_selected, @selected = @selected, selected + assert_select ":root", &block + ensure + @selected = old_selected + end + end + + # :call-seq: + # assert_select_email { } + # + # Extracts the body of an email and runs nested assertions on it. + # + # You must enable deliveries for this assertion to work, use: + # ActionMailer::Base.perform_deliveries = true + # + # === Example + # + # assert_select_email do + # assert_select "h1", "Email alert" + # end + def assert_select_email(&block) + deliveries = ActionMailer::Base.deliveries + assert !deliveries.empty?, "No e-mail in delivery list" + + for delivery in deliveries + for part in delivery.parts + if part["Content-Type"].to_s =~ /^text\/html\W/ + root = HTML::Document.new(part.body).root + assert_select root, ":root", &block + end + end + end + end + + protected + unless const_defined?(:RJS_STATEMENTS) + RJS_STATEMENTS = { + :replace => /Element\.replace/, + :replace_html => /Element\.update/ + } + RJS_INSERTIONS = [:top, :bottom, :before, :after] + RJS_INSERTIONS.each do |insertion| + RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}")) + end + RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") + RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do |insertion| + Regexp.quote("new Insertion.#{insertion.to_s.camelize}") + end.join('|')) + RJS_PATTERN_HTML = /"((\\"|[^"])*)"/ + RJS_PATTERN_EVERYTHING = Regexp.new("#{RJS_STATEMENTS[:any]}\\(\"([^\"]*)\", #{RJS_PATTERN_HTML}\\)", + Regexp::MULTILINE) + end + + # #assert_select and #css_select call this to obtain the content in the HTML + # page, or from all the RJS statements, depending on the type of response. + def response_from_page_or_rjs() + content_type = @response.headers["Content-Type"] + if content_type && content_type =~ /text\/javascript/ + body = @response.body.dup + root = HTML::Node.new(nil) + while true + next if body.sub!(RJS_PATTERN_EVERYTHING) do |match| + # RJS encodes double quotes and line breaks. + html = $3 + html.gsub!(/\\"/, "\"") + html.gsub!(/\\n/, "\n") + matches = HTML::Document.new(html).root.children.select { |n| n.tag? } + root.children.concat matches + "" + end + break + end + root + else + html_document.root + end + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_controller/assertions/tag_assertions.rb b/actionpack/lib/action_controller/assertions/tag_assertions.rb new file mode 100644 index 0000000000..dc80b442b7 --- /dev/null +++ b/actionpack/lib/action_controller/assertions/tag_assertions.rb @@ -0,0 +1,119 @@ +require 'test/unit' +require 'test/unit/assertions' +require 'rexml/document' +require File.dirname(__FILE__) + "/../vendor/html-scanner/html/document" + +module ActionController + module Assertions + module TagAssertions + # Asserts that there is a tag/node/element in the body of the response + # that meets all of the given conditions. The +conditions+ parameter must + # be a hash of any of the following keys (all are optional): + # + # * :tag: the node type must match the corresponding value + # * :attributes: a hash. The node's attributes must match the + # corresponding values in the hash. + # * :parent: a hash. The node's parent must match the + # corresponding hash. + # * :child: a hash. At least one of the node's immediate children + # must meet the criteria described by the hash. + # * :ancestor: a hash. At least one of the node's ancestors must + # meet the criteria described by the hash. + # * :descendant: a hash. At least one of the node's descendants + # must meet the criteria described by the hash. + # * :sibling: a hash. At least one of the node's siblings must + # meet the criteria described by the hash. + # * :after: a hash. The node must be after any sibling meeting + # the criteria described by the hash, and at least one sibling must match. + # * :before: a hash. The node must be before any sibling meeting + # the criteria described by the hash, and at least one sibling must match. + # * :children: a hash, for counting children of a node. Accepts + # the keys: + # * :count: either a number or a range which must equal (or + # include) the number of children that match. + # * :less_than: the number of matching children must be less + # than this number. + # * :greater_than: the number of matching children must be + # greater than this number. + # * :only: another hash consisting of the keys to use + # to match on the children, and only matching children will be + # counted. + # * :content: the textual content of the node must match the + # given value. This will not match HTML tags in the body of a + # tag--only text. + # + # Conditions are matched using the following algorithm: + # + # * if the condition is a string, it must be a substring of the value. + # * if the condition is a regexp, it must match the value. + # * if the condition is a number, the value must match number.to_s. + # * if the condition is +true+, the value must not be +nil+. + # * if the condition is +false+ or +nil+, the value must be +nil+. + # + # Usage: + # + # # assert that there is a "span" tag + # assert_tag :tag => "span" + # + # # assert that there is a "span" tag with id="x" + # assert_tag :tag => "span", :attributes => { :id => "x" } + # + # # assert that there is a "span" tag using the short-hand + # assert_tag :span + # + # # assert that there is a "span" tag with id="x" using the short-hand + # assert_tag :span, :attributes => { :id => "x" } + # + # # assert that there is a "span" inside of a "div" + # assert_tag :tag => "span", :parent => { :tag => "div" } + # + # # assert that there is a "span" somewhere inside a table + # assert_tag :tag => "span", :ancestor => { :tag => "table" } + # + # # assert that there is a "span" with at least one "em" child + # assert_tag :tag => "span", :child => { :tag => "em" } + # + # # assert that there is a "span" containing a (possibly nested) + # # "strong" tag. + # assert_tag :tag => "span", :descendant => { :tag => "strong" } + # + # # assert that there is a "span" containing between 2 and 4 "em" tags + # # as immediate children + # assert_tag :tag => "span", + # :children => { :count => 2..4, :only => { :tag => "em" } } + # + # # get funky: assert that there is a "div", with an "ul" ancestor + # # and an "li" parent (with "class" = "enum"), and containing a + # # "span" descendant that contains text matching /hello world/ + # assert_tag :tag => "div", + # :ancestor => { :tag => "ul" }, + # :parent => { :tag => "li", + # :attributes => { :class => "enum" } }, + # :descendant => { :tag => "span", + # :child => /hello world/ } + # + # Please noteYou must explicitly + # close all of your tags to use these assertions. + def assert_tag(*opts) + clean_backtrace do + opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first + tag = find_tag(opts) + assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" + end + end + + # Identical to #assert_tag, but asserts that a matching tag does _not_ + # exist. (See #assert_tag for a full discussion of the syntax.) + def assert_no_tag(*opts) + clean_backtrace do + opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first + tag = find_tag(opts) + assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" + end + end + end + end +end \ No newline at end of file -- cgit v1.2.3