From b24b33b3a223190085139b8fb14fdbf69f0e35b6 Mon Sep 17 00:00:00 2001 From: Brian Landau Date: Fri, 11 Jul 2008 17:35:05 -0400 Subject: [PATCH] Add assert_select_in method to ActionView::Assertions --- actionpack/lib/action_view/assertions.rb | 2 +- .../action_view/assertions/selector_assertions.rb | 230 ++++++++++++++++++++ actionpack/test/template/test_test.rb | 21 ++ 3 files changed, 252 insertions(+), 1 deletions(-) create mode 100644 actionpack/lib/action_view/assertions/selector_assertions.rb diff --git a/actionpack/lib/action_view/assertions.rb b/actionpack/lib/action_view/assertions.rb index b634731..ab0cdae 100644 --- a/actionpack/lib/action_view/assertions.rb +++ b/actionpack/lib/action_view/assertions.rb @@ -3,7 +3,7 @@ require 'test/unit/assertions' module ActionView #:nodoc: module Assertions def self.included(klass) - %w(tag).each do |kind| + %w(tag selector).each do |kind| require "action_view/assertions/#{kind}_assertions" klass.module_eval { include const_get("#{kind.camelize}Assertions") } end diff --git a/actionpack/lib/action_view/assertions/selector_assertions.rb b/actionpack/lib/action_view/assertions/selector_assertions.rb new file mode 100644 index 0000000..0aeef87 --- /dev/null +++ b/actionpack/lib/action_view/assertions/selector_assertions.rb @@ -0,0 +1,230 @@ +require 'action_controller' +require 'rexml/document' +require 'html/document' + +module ActionView + module Assertions + # Adds the +assert_select_in+ method for use in Rails helper + # test cases, which can be used to make assertions on the HTML of a helper + # method. You can also call +assert_select+ within another +assert_select+ to + # make assertions on elements selected by the enclosing assertion. + # + # Also see HTML::Selector to learn how to use selectors. + module SelectorAssertions + unless const_defined?(:NO_STRIP) + NO_STRIP = %w{pre script style textarea} + end + + # :call-seq: + # assert_select(target, selector, equality?, message?) => array + # + # An assertion that selects elements and makes one or more equality tests. + # + # The first parameter specifies the target HTML to which the selector should match, + # the second paramter is the selector to match with. + # Returns an empty array if no match is found. + # + # ==== Example + # assert_select_in html, "ol>li" do |elements| + # elements.each do |element| + # assert_select_in element, "li" + # end + # end + # + # Or for short: + # assert_select_in html, "ol>li" do + # assert_select_in "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: + # * true - Assertion is true if at least one element selected. + # * false - Assertion is true if no element selected. + # * String/Regexp - Assertion is true if the text value of at least + # one element matches the string or regular expression. + # * 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. + # If no equality test specified, the assertion is true if at least one + # element selected. + # + # To perform more than one equality tests, use a hash with the following keys: + # * :text - Narrow the selection to elements that have this text + # value (string or regexp). + # * :html - Narrow the selection to elements that have this HTML + # content (string or regexp). + # * :count - Assertion is true if the number of selected elements + # is equal to this value. + # * :minimum - Assertion is true if the number of selected + # elements is at least this value. + # * :maximum - Assertion is true if the number of selected + # elements is at most this 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_in html, "form" + # + # # Form element includes four input fields + # assert_select_in html, "form input", 4 + # + # # Page title is "Welcome" + # assert_select_in html, "title", "Welcome" + # + # # Page title is "Welcome" and there is only one title element + # assert_select_in html, "title", {:count=>1, :text=>"Welcome"}, + # "Wrong title or more than one title element" + # + # # Page contains no forms + # assert_select_in html, "form", false, "This page must contain no forms" + # + # # Test the content and style + # assert_select_in html, "body div.header ul.menu" + # + # # Use substitution values + # assert_select_in html, "ol>li#?", /item-\d+/ + # + # # All input fields in the form have a name + # assert_select_in html, "form input" do + # assert_select_in "[name=?]", /.+/ # Not empty + # end + # + def assert_select_in(*args, &block) + if @selected + root = HTML::Node.new(nil) + root.children.concat @selected + else + # Start with mandatory target. + target = args.shift + root = HTML::Document.new(target, false, false).root + end + + # Then get mandatory selector. + arg = args.shift + + # 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 + + # By default we're looking for at least one match. + if equals[:count] + equals[:minimum] = equals[:maximum] = equals[:count] + else + equals[:minimum] = 1 unless equals[:minimum] + 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) + + # If text/html, narrow down to those elements that match it. + content_mismatch = nil + if match_with = equals[:text] + matches.delete_if do |match| + text = "" + text.force_encoding(match_with.encoding) if text.respond_to?(:force_encoding) + stack = match.children.reverse + while node = stack.pop + if node.tag? + stack.concat node.children.reverse + else + content = node.content + content.force_encoding(match_with.encoding) if content.respond_to?(:force_encoding) + text << content + end + end + text.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) + content_mismatch ||= build_message(message, " expected but was\n.", match_with, text) + true + end + end + elsif match_with = equals[:html] + matches.delete_if do |match| + html = match.children.map(&:to_s).join + html.strip! unless NO_STRIP.include?(match.name) + unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) + content_mismatch ||= build_message(message, " expected but was\n.", match_with, html) + true + end + end + end + # Expecting foo found bar element only if found zero, not if + # found one but expecting two. + message ||= content_mismatch if matches.empty? + # Test minimum/maximum occurrence. + min, max = equals[:minimum], equals[:maximum] + message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.) + assert matches.size >= min, message if min + assert matches.size <= max, message if max + + # 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 + + private + def count_description(min, max) #:nodoc: + pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} + + if min && max && (max != min) + "between #{min} and #{max} elements" + elsif min && !(min == 1 && max == 1) + "at least #{min} #{pluralize['element', min]}" + elsif max + "at most #{max} #{pluralize['element', max]}" + end + end + end + end +end \ No newline at end of file diff --git a/actionpack/test/template/test_test.rb b/actionpack/test/template/test_test.rb index 05ffb90..983a14e 100644 --- a/actionpack/test/template/test_test.rb +++ b/actionpack/test/template/test_test.rb @@ -28,6 +28,27 @@ class AssertTagTest < ActiveSupport::TestCase end +class SelectorAssertionsTest < ActiveSupport::TestCase + + def test_assert_select_in_with_tags + html = '

hello world

' + assert_select_in html, 'p#test span' + end + + def test_assert_select_in_with_equality_test + html = '

hello world

' + assert_select_in html, 'p#test span', 'hello world' + end + + def test_assert_select_in_with_nested_selector + html = '
' + assert_select_in html, 'ul#list' do + assert_select_in 'li', 'one' + end + end + +end + module PeopleHelper def title(text) content_tag(:h1, text) -- 1.5.6.2