aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2005-04-17 16:43:48 +0000
committerDavid Heinemeier Hansson <david@loudthinking.com>2005-04-17 16:43:48 +0000
commit806cf6d76ab557ceebf5ac5d22f3e39076e1ab61 (patch)
tree5ca7149e3f8ee758cb3b0fd2df096baf61824153
parent95e6c03de7cd52a426560a6c46bd08b1791d7022 (diff)
downloadrails-806cf6d76ab557ceebf5ac5d22f3e39076e1ab61.tar.gz
rails-806cf6d76ab557ceebf5ac5d22f3e39076e1ab61.tar.bz2
rails-806cf6d76ab557ceebf5ac5d22f3e39076e1ab61.zip
Added assert_tag and assert_no_tag as a much improved alternative to the deprecated assert_template_xpath_match #1126 [Jamis Buck]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1195 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/action_controller/assertions.rb84
-rw-r--r--actionpack/lib/action_controller/test_process.rb22
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/document.rb63
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/node.rb431
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb95
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner/html/version.rb11
-rw-r--r--actionpack/test/controller/test_test.rb37
8 files changed, 744 insertions, 1 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index 338542b3db..6bafbf53c3 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -2,6 +2,8 @@
* Added Request#xml_http_request? (and an alias xhr?) to that'll return true when the request came from one of the Javascript helper methods (Ajax). This can be used to give one behavior for modern browsers supporting Ajax, another to old browsers #1127 [Sam Stephenson]
+* Added assert_tag and assert_no_tag as a much improved alternative to the deprecated assert_template_xpath_match #1126 [Jamis Buck]
+
* Deprecated the majority of all the testing assertions and replaced them with a much smaller core and access to all the collections the old assertions relied on. That way the regular test/unit assertions can be used against these. Added documentation about how to use it all.
* Fixed DateHelper to return values on the option tags such that they'll work properly in IE with form_remote_tag #1024 [rscottmace@gmail.com]
diff --git a/actionpack/lib/action_controller/assertions.rb b/actionpack/lib/action_controller/assertions.rb
index 38d551bca2..0c1ca76e19 100644
--- a/actionpack/lib/action_controller/assertions.rb
+++ b/actionpack/lib/action_controller/assertions.rb
@@ -116,6 +116,88 @@ module Test #:nodoc:
assert_recognizes(options, path, extras, message)
assert_generates(path, options, defaults, extras, message)
end
+
+ # 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):
+ #
+ # * <tt>:tag</tt>: the node type must match the corresponding value
+ # * <tt>:attributes</tt>: a hash. The node's attributes must match the
+ # corresponding values in the hash.
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
+ # corresponding hash.
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
+ # must meet the criteria described by the hash.
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
+ # meet the criteria described by the hash.
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
+ # must meet the criteria described by the hash.
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
+ # the keys:
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
+ # include) the number of children that match.
+ # ** <tt>:less_than</tt>: the number of matching children must be less
+ # than this number.
+ # ** <tt>:greater_than</tt>: the number of matching children must be
+ # greater than this number.
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
+ # to match on the children, and only matching children will be
+ # counted.
+ # * <tt>:content</tt>: (text nodes only). The content of the node must
+ # match the given value.
+ #
+ # 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" 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/ }
+ def assert_tag(opts)
+ tag = find_tag(opts)
+ assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
+ 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)
+ tag = find_tag(opts)
+ assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
+ end
+
end
end
-end \ No newline at end of file
+end
diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb
index 83cde64b85..a68887e8cc 100644
--- a/actionpack/lib/action_controller/test_process.rb
+++ b/actionpack/lib/action_controller/test_process.rb
@@ -243,6 +243,7 @@ module Test
private
# execute the request and set/volley the response
def process(action, parameters = nil, session = nil, flash = nil)
+ @html_document = nil
@request.env['REQUEST_METHOD'] ||= "GET"
@request.action = action.to_s
@request.path_parameters = { :controller => @controller.class.controller_path,
@@ -303,6 +304,27 @@ module Test
url.rewrite(@controller.send(:rewrite_options,
(parameters||{}).update(:only_path => true, :action=>action))))
end
+
+ def html_document
+ require_html_scanner
+ @html_document ||= HTML::Document.new(@response.body)
+ end
+
+ def find_tag(conditions)
+ html_document.find(conditions)
+ end
+
+ def find_all_tag(conditions)
+ html_document.find_all(conditions)
+ end
+
+ def require_html_scanner
+ return true if defined?(HTML::Document)
+ require 'html/document'
+ rescue LoadError
+ $:.unshift File.dirname(__FILE__) + "/vendor/html-scanner"
+ require 'html/document'
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb
new file mode 100644
index 0000000000..a196bdea44
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb
@@ -0,0 +1,63 @@
+require 'html/tokenizer'
+require 'html/node'
+
+module HTML
+
+ # A top-level HTMl document. You give it a body of text, and it will parse that
+ # text into a tree of nodes.
+ class Document
+
+ # The root of the parsed document.
+ attr_reader :root
+
+ # Create a new Document from the given text.
+ def initialize(text)
+ tokenizer = Tokenizer.new(text)
+ @root = Node.new(nil)
+ node_stack = [ @root ]
+ while token = tokenizer.next
+ node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token)
+
+ node_stack.last.children << node unless node.tag? && node.closing == :close
+ if node.tag? && !node.childless?
+ if node_stack.length > 1 && node.closing == :close
+ if node_stack.last.name == node.name
+ node_stack.pop
+ else
+ open_start = node_stack.last.position - 20
+ open_start = 0 if open_start < 0
+ close_start = node.position - 20
+ close_start = 0 if close_start < 0
+ warn <<EOF.strip
+ignoring attempt to close #{node_stack.last.name} with #{node.name}
+ opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
+ closed at byte #{node.position}, line #{node.line}
+ attributes at open: #{node_stack.last.attributes.inspect}
+ text around open: #{text[open_start,40].inspect}
+ text around close: #{text[close_start,40].inspect}
+EOF
+ end
+ elsif node.closing != :close
+ node_stack.push node
+ end
+ end
+ end
+ end
+
+ # Search the tree for (and return) the first node that matches the given
+ # conditions. The conditions are interpreted differently for different node
+ # types, see HTML::Text#find and HTML::Tag#find.
+ def find(conditions)
+ @root.find(conditions)
+ end
+
+ # Search the tree for (and return) all nodes that match the given
+ # conditions. The conditions are interpreted differently for different node
+ # types, see HTML::Text#find and HTML::Tag#find.
+ def find_all(conditions)
+ @root.find_all(conditions)
+ end
+
+ end
+
+end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb
new file mode 100644
index 0000000000..018309a9f9
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb
@@ -0,0 +1,431 @@
+require 'strscan'
+
+module HTML
+
+ class Conditions < Hash
+ def initialize(hash)
+ super()
+ hash = { :content => hash } unless Hash === hash
+ hash = keys_to_symbols(hash)
+ hash.each do |k,v|
+ case k
+ when :tag, :content then
+ # keys are valid, and require no further processing
+ when :attributes then
+ hash[k] = keys_to_strings(v)
+ when :parent, :child, :ancestor, :descendant
+ hash[k] = Conditions.new(v)
+ when :children
+ hash[k] = v = keys_to_symbols(v)
+ v.each do |k,v2|
+ case k
+ when :count, :greater_than, :less_than
+ # keys are valid, and require no further processing
+ when :only
+ v[k] = Conditions.new(v2)
+ else
+ raise "illegal key #{k.inspect} => #{v2.inspect}"
+ end
+ end
+ else
+ raise "illegal key #{k.inspect} => #{v.inspect}"
+ end
+ end
+ update hash
+ end
+
+ private
+
+ def keys_to_strings(hash)
+ hash.keys.inject({}) do |h,k|
+ h[k.to_s] = hash[k]
+ h
+ end
+ end
+
+ def keys_to_symbols(hash)
+ hash.keys.inject({}) do |h,k|
+ raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
+ h[k.to_sym] = hash[k]
+ h
+ end
+ end
+ end
+
+ # The base class of all nodes, textual and otherwise, in an HTML document.
+ class Node
+ # The array of children of this node. Not all nodes have children.
+ attr_reader :children
+
+ # The parent node of this node. All nodes have a parent, except for the
+ # root node.
+ attr_reader :parent
+
+ # The line number of the input where this node was begun
+ attr_reader :line
+
+ # The byte position in the input where this node was begun
+ attr_reader :position
+
+ # Create a new node as a child of the given parent.
+ def initialize(parent, line=0, pos=0)
+ @parent = parent
+ @children = []
+ @line, @position = line, pos
+ end
+
+ # Return a textual representation of the node.
+ def to_s
+ s = ""
+ @children.each { |child| s << child.to_s }
+ s
+ end
+
+ # Return false (subclasses must override this to provide specific matching
+ # behavior.) +conditions+ may be of any type.
+ def match(conditions)
+ false
+ end
+
+ # Search the children of this node for the first node for which #find
+ # returns non +nil+. Returns the result of the #find call that succeeded.
+ def find(conditions)
+ @children.each do |child|
+ node = child.find(conditions)
+ return node if node
+ end
+ nil
+ end
+
+ # Search for all nodes that match the given conditions, and return them
+ # as an array.
+ def find_all(conditions)
+ matches = []
+ matches << self if match(conditions)
+ @children.each do |child|
+ matches.concat child.find_all(conditions)
+ end
+ matches
+ end
+
+ # Returns +false+. Subclasses may override this if they define a kind of
+ # tag.
+ def tag?
+ false
+ end
+
+ def validate_conditions(conditions)
+ Conditions === conditions ? conditions : Conditions.new(conditions)
+ end
+
+ class <<self
+ def parse(parent, line, pos, content)
+ if content !~ /^<\S/
+ Text.new(parent, line, pos, content)
+ else
+ scanner = StringScanner.new(content)
+ scanner.skip(/</) or raise "expected <"
+ closing = ( scanner.scan(/\//) ? :close : nil )
+ return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
+ name.downcase!
+
+ unless closing
+ scanner.skip(/\s*/)
+ attributes = {}
+ while attr = scanner.scan(/[-\w:]+/)
+ value = true
+ if scanner.scan(/\s*=\s*/)
+ if delim = scanner.scan(/['"]/)
+ value = ""
+ while text = scanner.scan(/[^#{delim}\\]+|./)
+ case text
+ when "\\" then
+ value << text
+ value << scanner.getch
+ when delim
+ break
+ else value << text
+ end
+ end
+ else
+ value = scanner.scan(/[^\s>\/]+/)
+ end
+ end
+ attributes[attr.downcase] = value
+ scanner.skip(/\s*/)
+ end
+
+ closing = ( scanner.scan(/\//) ? :self : nil )
+ end
+
+ scanner.scan(/\s*>/) or raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
+
+ Tag.new(parent, line, pos, name, attributes, closing)
+ end
+ end
+ end
+ end
+
+ # A node that represents text, rather than markup.
+ class Text < Node
+
+ attr_reader :content
+
+ # Creates a new text node as a child of the given parent, with the given
+ # content.
+ def initialize(parent, line, pos, content)
+ super(parent, line, pos)
+ @content = content
+ end
+
+ # Returns the content of this node.
+ def to_s
+ @content
+ end
+
+ # Returns +self+ if this node meets the given conditions. Text nodes support
+ # conditions of the following kinds:
+ #
+ # * if +conditions+ is a string, it must be a substring of the node's
+ # content
+ # * if +conditions+ is a regular expression, it must match the node's
+ # content
+ # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
+ # is either a string or a regexp, and which is interpreted as described
+ # above.
+ def find(conditions)
+ match(conditions) && self
+ end
+
+ # Returns non-+nil+ if this node meets the given conditions, or +nil+
+ # otherwise. See the discussion of #find for the valid conditions.
+ def match(conditions)
+ case conditions
+ when String
+ @content.index(conditions)
+ when Regexp
+ @content =~ conditions
+ when Hash
+ conditions = validate_conditions(conditions)
+
+ # Text nodes only have :content, :parent, :ancestor
+ unless (conditions.keys - [:content, :parent, :ancestor]).empty?
+ return false
+ end
+
+ match(conditions[:content])
+ else
+ nil
+ end
+ end
+ end
+
+ # A Tag is any node that represents markup. It may be an opening tag, a
+ # closing tag, or a self-closing tag. It has a name, and may have a hash of
+ # attributes.
+ class Tag < Node
+
+ # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
+ attr_reader :closing
+
+ # Either +nil+, or a hash of attributes for this node.
+ attr_reader :attributes
+
+ # The name of this tag.
+ attr_reader :name
+
+ # Create a new node as a child of the given parent, using the given content
+ # to describe the node. It will be parsed and the node name, attributes and
+ # closing status extracted.
+ def initialize(parent, line, pos, name, attributes, closing)
+ super(parent, line, pos)
+ @name = name
+ @attributes = attributes
+ @closing = closing
+ end
+
+ # A convenience for obtaining an attribute of the node. Returns +nil+ if
+ # the node has no attributes.
+ def [](attr)
+ @attributes ? @attributes[attr] : nil
+ end
+
+ # Returns non-+nil+ if this tag can contain child nodes.
+ def childless?
+ @name =~ /^(img|br|hr|link|meta|area|base|basefont|col|frame|input|isindex|param)$/o
+ end
+
+ # Returns a textual representation of the node
+ def to_s
+ if @closing == :close
+ "</#{@name}>"
+ else
+ s = "<#{@name}"
+ @attributes.each { |k,v| s << " #{k}='#{v.gsub(/'/,"\\\\'")}'" }
+ s << " /" if @closing == :self
+ s << ">"
+ @children.each { |child| s << child.to_s }
+ s
+ end
+ end
+
+ # If either the node or any of its children meet the given conditions, the
+ # matching node is returned. Otherwise, +nil+ is returned. (See the
+ # description of the valid conditions in the +match+ method.)
+ def find(conditions)
+ match(conditions) && self || super
+ end
+
+ # Returns +true+, indicating that this node represents an HTML tag.
+ def tag?
+ true
+ end
+
+ # Returns +true+ if the node meets any of the given conditions. The
+ # +conditions+ parameter must be a hash of any of the following keys
+ # (all are optional):
+ #
+ # * <tt>:tag</tt>: the node name must match the corresponding value
+ # * <tt>:attributes</tt>: a hash. The node's values must match the
+ # corresponding values in the hash.
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
+ # corresponding hash.
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
+ # must meet the criteria described by the hash.
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
+ # meet the criteria described by the hash.
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
+ # must meet the criteria described by the hash.
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
+ # keys:
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
+ # include) the number of children that match.
+ # ** <tt>:less_than</tt>: the number of matching children must be less than
+ # this number.
+ # ** <tt>:greater_than</tt>: the number of matching children must be
+ # greater than this number.
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
+ # to match on the children, and only matching children will be
+ # counted.
+ #
+ # 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:
+ #
+ # # test if the node is a "span" tag
+ # node.match :tag => "span"
+ #
+ # # test if the node's parent is a "div"
+ # node.match :parent => { :tag => "div" }
+ #
+ # # test if any of the node's ancestors are "table" tags
+ # node.match :ancestor => { :tag => "table" }
+ #
+ # # test if any of the node's immediate children are "em" tags
+ # node.match :child => { :tag => "em" }
+ #
+ # # test if any of the node's descendants are "strong" tags
+ # node.match :descendant => { :tag => "strong" }
+ #
+ # # test if the node has between 2 and 4 span tags as immediate children
+ # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
+ #
+ # # get funky: test to see if the node is a "div", has a "ul" ancestor
+ # # and an "li" parent (with "class" = "enum"), and whether or not it has
+ # # a "span" descendant that contains # text matching /hello world/:
+ # node.match :tag => "div",
+ # :ancestor => { :tag => "ul" },
+ # :parent => { :tag => "li",
+ # :attributes => { :class => "enum" } },
+ # :descendant => { :tag => "span",
+ # :child => /hello world/ }
+ def match(conditions)
+ conditions = validate_conditions(conditions)
+
+ # only Text nodes have content
+ return false if conditions[:content]
+
+ # test the name
+ return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
+
+ # test attributes
+ (conditions[:attributes] || {}).each do |key, value|
+ return false unless match_condition(self[key], value)
+ end
+
+ # test parent
+ return false unless parent.match(conditions[:parent]) if conditions[:parent]
+
+ # test children
+ return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
+
+ # test ancestors
+ if conditions[:ancestor]
+ return false unless catch :found do
+ p = self
+ throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
+ end
+ end
+
+ # test descendants
+ if conditions[:descendant]
+ return false unless children.find do |child|
+ # test the child
+ child.match(conditions[:descendant]) ||
+ # test the child's descendants
+ child.match(:descendant => conditions[:descendant])
+ end
+ end
+
+ # count children
+ if opts = conditions[:children]
+ matches = children
+ matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
+ opts.each do |key, value|
+ next if key == :only
+ case key
+ when :count
+ if Integer === value
+ return false if matches.length != value
+ else
+ return false unless value.include?(matches.length)
+ end
+ when :less_than
+ return false unless matches.length < value
+ when :greater_than
+ return false unless matches.length > value
+ else raise "unknown count condition #{key}"
+ end
+ end
+ end
+
+ true
+ end
+
+ private
+
+ # Match the given value to the given condition.
+ def match_condition(value, condition)
+ case condition
+ when String
+ value && value.index(condition)
+ when Regexp
+ value && value.match(condition)
+ when Numeric
+ value == condition.to_s
+ when true
+ !value.nil?
+ when false, nil
+ value.nil?
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb
new file mode 100644
index 0000000000..11bd48708e
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb
@@ -0,0 +1,95 @@
+require 'strscan'
+
+module HTML
+
+ # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
+ # token is a string. Each string represents either "text", or an HTML element.
+ #
+ # This currently assumes valid XHTML, which means no free < or > characters.
+ #
+ # Usage:
+ #
+ # tokenizer = HTML::Tokenizer.new(text)
+ # while token = tokenizer.next
+ # p token
+ # end
+ class Tokenizer
+
+ # The current (byte) position in the text
+ attr_reader :position
+
+ # The current line number
+ attr_reader :line
+
+ # Create a new Tokenizer for the given text.
+ def initialize(text)
+ @scanner = StringScanner.new(text)
+ @position = 0
+ @line = 0
+ @current_line = 1
+ end
+
+ # Return the next token in the sequence, or +nil+ if there are no more tokens in
+ # the stream.
+ def next
+ return nil if @scanner.eos?
+ @position = @scanner.pos
+ @line = @current_line
+ if @scanner.check(/<\S/)
+ update_current_line(scan_tag)
+ else
+ update_current_line(scan_text)
+ end
+ end
+
+ private
+
+ # Treat the text at the current position as a tag, and scan it. Supports
+ # comments, doctype tags, and regular tags, and ignores less-than and
+ # greater-than characters within quoted strings.
+ def scan_tag
+ tag = @scanner.getch
+ if @scanner.scan(/!--/) # comment
+ tag << @scanner.matched
+ tag << @scanner.scan_until(/--\s*>/)
+ elsif @scanner.scan(/!/) # doctype
+ tag << @scanner.matched
+ tag << consume_quoted_regions
+ else
+ tag << consume_quoted_regions
+ end
+ tag
+ end
+
+ # Scan all text up to the next < character and return it.
+ def scan_text
+ @scanner.scan(/[^<]*/)
+ end
+
+ # Counts the number of newlines in the text and updates the current line
+ # accordingly.
+ def update_current_line(text)
+ @current_line += text.scan(/\r\n|\r|\n/).length
+ text
+ end
+
+ # Skips over quoted strings, so that less-than and greater-than characters
+ # within the strings are ignored.
+ def consume_quoted_regions
+ text = ""
+ loop do
+ match = @scanner.scan_until(/['">]/) or break
+ text << match
+ break if (delim = @scanner.matched) == ">"
+ # consume the conqued region
+ while match = @scanner.scan_until(/[\\#{delim}]/)
+ text << match
+ break if @scanner.matched == delim
+ text << @scanner.getch # skip the escaped character
+ end
+ end
+ text
+ end
+ end
+
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb
new file mode 100644
index 0000000000..90025fa57f
--- /dev/null
+++ b/actionpack/lib/action_controller/vendor/html-scanner/html/version.rb
@@ -0,0 +1,11 @@
+module HTML
+ module Version
+
+ MAJOR = 0
+ MINOR = 5
+ TINY = 0
+
+ STRING = [ MAJOR, MINOR, TINY ].join(".")
+
+ end
+end
diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb
index f92e9dae48..7b35447da5 100644
--- a/actionpack/test/controller/test_test.rb
+++ b/actionpack/test/controller/test_test.rb
@@ -9,6 +9,26 @@ class TestTest < Test::Unit::TestCase
def test_uri
render_text @request.request_uri
end
+
+ def test_html_output
+ render_text <<HTML
+<html>
+ <body>
+ <div id="foo">
+ <ul>
+ <li class="item">hello</li>
+ <li class="item">goodbye</li>
+ </ul>
+ </div>
+ <div id="bar">
+ <form action="/somewhere">
+ Name: <input type="text" name="person[name]" id="person_name" />
+ </form>
+ </div>
+ </body>
+</html>
+HTML
+ end
end
def setup
@@ -47,4 +67,21 @@ class TestTest < Test::Unit::TestCase
process :test_uri, :id => 7
assert_equal @response.body, "/explicit/uri"
end
+
+ def test_assert_tag
+ process :test_html_output
+
+ # there is a 'div', id='bar', with an immediate child whose 'action'
+ # attribute matches the regexp /somewhere/.
+ assert_tag :tag => "div", :attributes => { :id => "bar" },
+ :child => { :attributes => { :action => /somewhere/ } }
+
+ # there is no 'div', id='foo', with a 'ul' child with more than
+ # 2 "li" children.
+ assert_no_tag :tag => "div", :attributes => { :id => "foo" },
+ :child => {
+ :tag => "ul",
+ :children => { :greater_than => 2,
+ :only => { :tag => "li" } } }
+ end
end