mirror of
https://github.com/github/rails.git
synced 2026-04-04 03:00:58 -04:00
Added assert_select* for CSS selector-based testing (deprecates assert_tag) #5936 [assaf.arkin@gmail.com]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4929 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
*SVN*
|
||||
|
||||
* Added assert_select* for CSS selector-based testing (deprecates assert_tag) #5936 [assaf.arkin@gmail.com]
|
||||
|
||||
* radio_button_tag generates unique id attributes. #3353 [Bob Silva, somekool@gmail.com]
|
||||
|
||||
* strip_tags returns nil for a blank arg such as nil or "". #2229 [duncan@whomwah.com]
|
||||
|
||||
557
actionpack/lib/action_controller/assert_select.rb
Normal file
557
actionpack/lib/action_controller/assert_select.rb
Normal file
@@ -0,0 +1,557 @@
|
||||
#--
|
||||
# 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:
|
||||
# * <tt>nil/true</tt> -- Assertion is true if at least one element is
|
||||
# selected.
|
||||
# * <tt>String</tt> -- Assertion is true if the text value of all
|
||||
# selected elements equals to the string.
|
||||
# * <tt>Regexp</tt> -- Assertion is true if the text value of all
|
||||
# selected elements matches the regular expression.
|
||||
# * <tt>false</tt> -- Assertion is true if no element is selected.
|
||||
# * <tt>Integer</tt> -- Assertion is true if exactly that number of
|
||||
# elements are selected.
|
||||
# * <tt>Range</tt> -- 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:
|
||||
# * <tt>:text</tt> -- Assertion is true if the text value of each
|
||||
# selected elements equals to the value (+String+ or +Regexp+).
|
||||
# * <tt>:count</tt> -- Assertion is true if the number of matched elements
|
||||
# is equal to the value.
|
||||
# * <tt>:minimum</tt> -- Assertion is true if the number of matched
|
||||
# elements is at least that value.
|
||||
# * <tt>:maximum</tt> -- 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, <<EOT, value, text)
|
||||
<?> 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, <<EOT, value, html)
|
||||
<?> 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(/<!\[CDATA\[(.*)(\]\]>)?/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("<encoded>#{text}</encoded>")).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
|
||||
|
||||
Test::Unit::TestCase.send :include, ActionController::Assertions::SelectorAssertions
|
||||
121
actionpack/lib/action_controller/assert_tag.rb
Normal file
121
actionpack/lib/action_controller/assert_tag.rb
Normal file
@@ -0,0 +1,121 @@
|
||||
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):
|
||||
#
|
||||
# * <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>:sibling</tt>: a hash. At least one of the node's siblings must
|
||||
# meet the criteria described by the hash.
|
||||
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
|
||||
# the criteria described by the hash, and at least one sibling must match.
|
||||
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
|
||||
# the criteria described by the hash, and at least one sibling must match.
|
||||
# * <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>: 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/ }
|
||||
#
|
||||
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
|
||||
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
|
||||
# (like br and hr and such) but will not work correctly with tags
|
||||
# that allow optional closing tags (p, li, td). <em>You must explicitly
|
||||
# close all of your tags to use these assertions.</em>
|
||||
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
|
||||
|
||||
Test::Unit::TestCase.send :include, ActionController::Assertions::TagAssertions
|
||||
@@ -221,115 +221,6 @@ module Test #:nodoc:
|
||||
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>:sibling</tt>: a hash. At least one of the node's siblings must
|
||||
# meet the criteria described by the hash.
|
||||
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
|
||||
# the criteria described by the hash, and at least one sibling must match.
|
||||
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
|
||||
# the criteria described by the hash, and at least one sibling must match.
|
||||
# * <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>: 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/ }
|
||||
#
|
||||
# <strong>Please note</strong: #assert_tag and #assert_no_tag only work
|
||||
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
|
||||
# (like br and hr and such) but will not work correctly with tags
|
||||
# that allow optional closing tags (p, li, td). <em>You must explicitly
|
||||
# close all of your tags to use these assertions.</em>
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -382,4 +273,4 @@ module Test #:nodoc:
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
require File.dirname(__FILE__) + '/assertions'
|
||||
require File.dirname(__FILE__) + '/assert_select'
|
||||
require File.dirname(__FILE__) + '/assert_tag'
|
||||
require File.dirname(__FILE__) + '/deprecated_assertions'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require File.dirname(__FILE__) + '/tokenizer'
|
||||
require File.dirname(__FILE__) + '/node'
|
||||
require File.dirname(__FILE__) + '/selector'
|
||||
|
||||
module HTML #:nodoc:
|
||||
|
||||
|
||||
822
actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
vendored
Normal file
822
actionpack/lib/action_controller/vendor/html-scanner/html/selector.rb
vendored
Normal file
@@ -0,0 +1,822 @@
|
||||
#--
|
||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
||||
# Under MIT and/or CC By license.
|
||||
#++
|
||||
|
||||
module HTML
|
||||
|
||||
# Selects HTML elements using CSS 2 selectors.
|
||||
#
|
||||
# The +Selector+ class uses CSS selector expressions to match and select
|
||||
# HTML elements.
|
||||
#
|
||||
# For example:
|
||||
# selector = HTML::Selector.new "form.login[action=/login]"
|
||||
# creates a new selector that matches any +form+ element with the class
|
||||
# +login+ and an attribute +action+ with the value <tt>/login</tt>.
|
||||
#
|
||||
# === Matching Elements
|
||||
#
|
||||
# Use the #match method to determine if an element matches the selector.
|
||||
#
|
||||
# For simple selectors, the method returns an array with that element,
|
||||
# or +nil+ if the element does not match. For complex selectors (see below)
|
||||
# the method returns an array with all matched elements, of +nil+ if no
|
||||
# match found.
|
||||
#
|
||||
# For example:
|
||||
# if selector.match(element)
|
||||
# puts "Element is a login form"
|
||||
# end
|
||||
#
|
||||
# === Selecting Elements
|
||||
#
|
||||
# Use the #select method to select all matching elements starting with
|
||||
# one element and going through all children in depth-first order.
|
||||
#
|
||||
# This method returns an array of all matching elements, an empty array
|
||||
# if no match is found
|
||||
#
|
||||
# For example:
|
||||
# selector = HTML::Selector.new "input[type=text]"
|
||||
# matches = selector.select(element)
|
||||
# matches.each do |match|
|
||||
# puts "Found text field with name #{match.attributes['name']}"
|
||||
# end
|
||||
#
|
||||
# === Expressions
|
||||
#
|
||||
# Selectors can match elements using any of the following criteria:
|
||||
# * <tt>name</tt> -- Match an element based on its name (tag name).
|
||||
# For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
|
||||
# to match any element.
|
||||
# * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
|
||||
# <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
|
||||
# * <tt>.class</tt> -- Match an element based on its class name, all
|
||||
# class names if more than one specified.
|
||||
# * <tt>[attr]</tt> -- Match an element that has the specified attribute.
|
||||
# * <tt>[attr=value]</tt> -- Match an element that has the specified
|
||||
# attribute and value. (More operators are supported see below)
|
||||
# * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
|
||||
# such as <tt>:nth-child</tt> and <tt>:empty</tt>.
|
||||
# * <tt>:not(expr)</tt> -- Match an element that does not match the
|
||||
# negation expression.
|
||||
#
|
||||
# When using a combination of the above, the element name comes first
|
||||
# followed by identifier, class names, attributes, pseudo classes and
|
||||
# negation in any order. Do not seprate these parts with spaces!
|
||||
# Space separation is used for descendant selectors.
|
||||
#
|
||||
# For example:
|
||||
# selector = HTML::Selector.new "form.login[action=/login]"
|
||||
# The matched element must be of type +form+ and have the class +login+.
|
||||
# It may have other classes, but the class +login+ is required to match.
|
||||
# It must also have an attribute called +action+ with the value
|
||||
# <tt>/login</tt>.
|
||||
#
|
||||
# This selector will match the following element:
|
||||
# <form class="login form" method="post" action="/login">
|
||||
# but will not match the element:
|
||||
# <form method="post" action="/logout">
|
||||
#
|
||||
# === Attribute Values
|
||||
#
|
||||
# Several operators are supported for matching attributes:
|
||||
# * <tt>name</tt> -- The element must have an attribute with that name.
|
||||
# * <tt>name=value</tt> -- The element must have an attribute with that
|
||||
# name and value.
|
||||
# * <tt>name^=value</tt> -- The attribute value must start with the
|
||||
# specified value.
|
||||
# * <tt>name$=value</tt> -- The attribute value must end with the
|
||||
# specified value.
|
||||
# * <tt>name*=value</tt> -- The attribute value must contain the
|
||||
# specified value.
|
||||
# * <tt>name~=word</tt> -- The attribute value must contain the specified
|
||||
# word (space separated).
|
||||
# * <tt>name|=word</tt> -- The attribute value must start with specified
|
||||
# word.
|
||||
#
|
||||
# For example, the following two selectors match the same element:
|
||||
# #my_id
|
||||
# [id=my_id]
|
||||
# and so do the following two selectors:
|
||||
# .my_class
|
||||
# [class~=my_class]
|
||||
#
|
||||
# === Alternatives, siblings, children
|
||||
#
|
||||
# Complex selectors use a combination of expressions to match elements:
|
||||
# * <tt>expr1 expr2</tt> -- Match any element against the second expression
|
||||
# if it has some parent element that matches the first expression.
|
||||
# * <tt>expr1 > expr2</tt> -- Match any element against the second expression
|
||||
# if it is the child of an element that matches the first expression.
|
||||
# * <tt>expr1 + expr2</tt> -- Match any element against the second expression
|
||||
# if it immediately follows an element that matches the first expression.
|
||||
# * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
|
||||
# that comes after an element that matches the first expression.
|
||||
# * <tt>expr1, expr2</tt> -- Match any element against the first expression,
|
||||
# or against the second expression.
|
||||
#
|
||||
# Since children and sibling selectors may match more than one element given
|
||||
# the first element, the #match method may return more than one match.
|
||||
#
|
||||
# === Pseudo classes
|
||||
#
|
||||
# Pseudo classes were introduced in CSS 3. They are most often used to select
|
||||
# elements in a given position:
|
||||
# * <tt>:root</tt> -- Match the element only if it is the root element
|
||||
# (no parent element).
|
||||
# * <tt>:empty</tt> -- Match the element only if it has no child elements,
|
||||
# and no text content.
|
||||
# * <tt>:only-child</tt> -- Match the element if it is the only child (element)
|
||||
# of its parent element.
|
||||
# * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
|
||||
# of its parent element and its type.
|
||||
# * <tt>:first-child</tt> -- Match the element if it is the first child (element)
|
||||
# of its parent element.
|
||||
# * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
|
||||
# of its parent element of its type.
|
||||
# * <tt>:last-child</tt> -- Match the element if it is the last child (element)
|
||||
# of its parent element.
|
||||
# * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
|
||||
# of its parent element of its type.
|
||||
# * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
|
||||
# of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
|
||||
# * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
|
||||
# in each group of <tt>a</tt> child elements of its parent element.
|
||||
# * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
|
||||
# in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
|
||||
# elements of its parent element.
|
||||
# * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
|
||||
# Same as <tt>:nth-child(2n+1)</tt>.
|
||||
# * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
|
||||
# fourth). Same as <tt>:nth-child(2n+2)</tt>.
|
||||
# * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
|
||||
# * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
|
||||
# * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
|
||||
# only elements of its type.
|
||||
# * <tt>:not(selector)</tt> -- Match the element only if the element does not
|
||||
# match the simple selector.
|
||||
#
|
||||
# As you can see, <tt>:nth-child<tt> pseudo class and its varient can get quite
|
||||
# tricky and the CSS specification doesn't do a much better job explaining it.
|
||||
# But after reading the examples and trying a few combinations, it's easy to
|
||||
# figure out.
|
||||
#
|
||||
# For example:
|
||||
# table tr:nth-child(odd)
|
||||
# Selects every second row in the table starting with the first one.
|
||||
#
|
||||
# div p:nth-child(4)
|
||||
# Selects the fourth paragraph in the +div+, but not if the +div+ contains
|
||||
# other elements, since those are also counted.
|
||||
#
|
||||
# div p:nth-of-type(4)
|
||||
# Selects the fourth paragraph in the +div+, counting only paragraphs, and
|
||||
# ignoring all other elements.
|
||||
#
|
||||
# div p:nth-of-type(-n+4)
|
||||
# Selects the first four paragraphs, ignoring all others.
|
||||
#
|
||||
# And you can always select an element that matches one set of rules but
|
||||
# not another using <tt>:not</tt>. For example:
|
||||
# p:not(.post)
|
||||
# Matches all paragraphs that do not have the class <tt>.post</tt>.
|
||||
#
|
||||
# === Substitution Values
|
||||
#
|
||||
# You can use substitution with identifiers, class names and element values.
|
||||
# A substitution takes the form of a question mark (<tt>?</tt>) and uses the
|
||||
# next value in the argument list following the CSS expression.
|
||||
#
|
||||
# The substitution value may be a string or a regular expression. All other
|
||||
# values are converted to strings.
|
||||
#
|
||||
# For example:
|
||||
# selector = HTML::Selector.new "#?", /^\d+$/
|
||||
# matches any element whose identifier consists of one or more digits.
|
||||
#
|
||||
# See http://www.w3.org/TR/css3-selectors/
|
||||
class Selector
|
||||
|
||||
|
||||
# An invalid selector.
|
||||
class InvalidSelectorError < StandardError ; end
|
||||
|
||||
|
||||
class << self
|
||||
|
||||
# :call-seq:
|
||||
# Selector.for_class(cls) => selector
|
||||
#
|
||||
# Creates a new selector for the given class name.
|
||||
def for_class(cls)
|
||||
self.new([".?", cls])
|
||||
end
|
||||
|
||||
|
||||
# :call-seq:
|
||||
# Selector.for_id(id) => selector
|
||||
#
|
||||
# Creates a new selector for the given id.
|
||||
def for_id(id)
|
||||
self.new(["#?", id])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# :call-seq:
|
||||
# Selector.new(string, [values ...]) => selector
|
||||
#
|
||||
# Creates a new selector from a CSS 2 selector expression.
|
||||
#
|
||||
# The first argument is the selector expression. All other arguments
|
||||
# are used for value substitution.
|
||||
#
|
||||
# Throws InvalidSelectorError is the selector expression is invalid.
|
||||
def initialize(selector, *values)
|
||||
raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
|
||||
@source = ""
|
||||
values = values[0] if values.size == 1 && values[0].is_a?(Array)
|
||||
# We need a copy to determine if we failed to parse, and also
|
||||
# preserve the original pass by-ref statement.
|
||||
statement = selector.strip.dup
|
||||
# Create a simple selector, along with negation.
|
||||
simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
|
||||
|
||||
# Alternative selector.
|
||||
if statement.sub!(/^\s*,\s*/, "")
|
||||
second = Selector.new(statement, values)
|
||||
(@alternates ||= []) << second
|
||||
# If there are alternate selectors, we group them in the top selector.
|
||||
if alternates = second.instance_variable_get(:@alternates)
|
||||
second.instance_variable_set(:@alternates, nil)
|
||||
@alternates.concat alternates
|
||||
end
|
||||
@source << " , " << second.to_s
|
||||
# Sibling selector: create a dependency into second selector that will
|
||||
# match element immediately following this one.
|
||||
elsif statement.sub!(/^\s*\+\s*/, "")
|
||||
second = next_selector(statement, values)
|
||||
@depends = lambda do |element, first|
|
||||
if element = next_element(element)
|
||||
second.match(element, first)
|
||||
end
|
||||
end
|
||||
@source << " + " << second.to_s
|
||||
# Adjacent selector: create a dependency into second selector that will
|
||||
# match all elements following this one.
|
||||
elsif statement.sub!(/^\s*~\s*/, "")
|
||||
second = next_selector(statement, values)
|
||||
@depends = lambda do |element, first|
|
||||
matches = []
|
||||
while element = next_element(element)
|
||||
if subset = second.match(element, first)
|
||||
if first && !subset.empty?
|
||||
matches << subset.first
|
||||
break
|
||||
else
|
||||
matches.concat subset
|
||||
end
|
||||
end
|
||||
end
|
||||
matches.empty? ? nil : matches
|
||||
end
|
||||
@source << " ~ " << second.to_s
|
||||
# Child selector: create a dependency into second selector that will
|
||||
# match a child element of this one.
|
||||
elsif statement.sub!(/^\s*>\s*/, "")
|
||||
second = next_selector(statement, values)
|
||||
@depends = lambda do |element, first|
|
||||
matches = []
|
||||
element.children.each do |child|
|
||||
if child.tag? && subset = second.match(child, first)
|
||||
if first && !subset.empty?
|
||||
matches << subset.first
|
||||
break
|
||||
else
|
||||
matches.concat subset
|
||||
end
|
||||
end
|
||||
end
|
||||
matches.empty? ? nil : matches
|
||||
end
|
||||
@source << " > " << second.to_s
|
||||
# Descendant selector: create a dependency into second selector that
|
||||
# will match all descendant elements of this one. Note,
|
||||
elsif statement =~ /^\s+\S+/ && statement != selector
|
||||
second = next_selector(statement, values)
|
||||
@depends = lambda do |element, first|
|
||||
matches = []
|
||||
stack = element.children.reverse
|
||||
while node = stack.pop
|
||||
next unless node.tag?
|
||||
if subset = second.match(node, first)
|
||||
if first && !subset.empty?
|
||||
matches << subset.first
|
||||
break
|
||||
else
|
||||
matches.concat subset
|
||||
end
|
||||
elsif children = node.children
|
||||
stack.concat children.reverse
|
||||
end
|
||||
end
|
||||
matches.empty? ? nil : matches
|
||||
end
|
||||
@source << " " << second.to_s
|
||||
else
|
||||
# The last selector is where we check that we parsed
|
||||
# all the parts.
|
||||
unless statement.empty? || statement.strip.empty?
|
||||
raise ArgumentError, "Invalid selector: #{statement}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# :call-seq:
|
||||
# match(element, first?) => array or nil
|
||||
#
|
||||
# Matches an element against the selector.
|
||||
#
|
||||
# For a simple selector this method returns an array with the
|
||||
# element if the element matches, nil otherwise.
|
||||
#
|
||||
# For a complex selector (sibling and descendant) this method
|
||||
# returns an array with all matching elements, nil if no match is
|
||||
# found.
|
||||
#
|
||||
# Use +first_only=true+ if you are only interested in the first element.
|
||||
#
|
||||
# For example:
|
||||
# if selector.match(element)
|
||||
# puts "Element is a login form"
|
||||
# end
|
||||
def match(element, first_only = false)
|
||||
# Match element if no element name or element name same as element name
|
||||
if matched = (!@tag_name || @tag_name == element.name)
|
||||
# No match if one of the attribute matches failed
|
||||
for attr in @attributes
|
||||
if element.attributes[attr[0]] !~ attr[1]
|
||||
matched = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Pseudo class matches (nth-child, empty, etc).
|
||||
if matched
|
||||
for pseudo in @pseudo
|
||||
unless pseudo.call(element)
|
||||
matched = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Negation. Same rules as above, but we fail if a match is made.
|
||||
if matched && @negation
|
||||
for negation in @negation
|
||||
if negation[:tag_name] == element.name
|
||||
matched = false
|
||||
else
|
||||
for attr in negation[:attributes]
|
||||
if element.attributes[attr[0]] =~ attr[1]
|
||||
matched = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if matched
|
||||
for pseudo in negation[:pseudo]
|
||||
if pseudo.call(element)
|
||||
matched = false
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
break unless matched
|
||||
end
|
||||
end
|
||||
|
||||
# If element matched but depends on another element (child,
|
||||
# sibling, etc), apply the dependent matches instead.
|
||||
if matched && @depends
|
||||
matches = @depends.call(element, first_only)
|
||||
else
|
||||
matches = matched ? [element] : nil
|
||||
end
|
||||
|
||||
# If this selector is part of the group, try all the alternative
|
||||
# selectors (unless first_only).
|
||||
if @alternates && (!first_only || !matches)
|
||||
@alternates.each do |alternate|
|
||||
break if matches && first_only
|
||||
if subset = alternate.match(element, first_only)
|
||||
if matches
|
||||
matches.concat subset
|
||||
else
|
||||
matches = subset
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
matches
|
||||
end
|
||||
|
||||
|
||||
# :call-seq:
|
||||
# select(root) => array
|
||||
#
|
||||
# Selects and returns an array with all matching elements, beginning
|
||||
# with one node and traversing through all children depth-first.
|
||||
# Returns an empty array if no match is found.
|
||||
#
|
||||
# The root node may be any element in the document, or the document
|
||||
# itself.
|
||||
#
|
||||
# For example:
|
||||
# selector = HTML::Selector.new "input[type=text]"
|
||||
# matches = selector.select(element)
|
||||
# matches.each do |match|
|
||||
# puts "Found text field with name #{match.attributes['name']}"
|
||||
# end
|
||||
def select(root)
|
||||
matches = []
|
||||
stack = [root]
|
||||
while node = stack.pop
|
||||
if node.tag? && subset = match(node, false)
|
||||
subset.each do |match|
|
||||
matches << match unless matches.any? { |item| item.equal?(match) }
|
||||
end
|
||||
elsif children = node.children
|
||||
stack.concat children.reverse
|
||||
end
|
||||
end
|
||||
matches
|
||||
end
|
||||
|
||||
|
||||
# Similar to #select but returns the first matching element. Returns +nil+
|
||||
# if no element matches the selector.
|
||||
def select_first(root)
|
||||
stack = [root]
|
||||
while node = stack.pop
|
||||
if node.tag? && subset = match(node, true)
|
||||
return subset.first if !subset.empty?
|
||||
elsif children = node.children
|
||||
stack.concat children.reverse
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
def to_s #:nodoc:
|
||||
@source
|
||||
end
|
||||
|
||||
|
||||
# Return the next element after this one. Skips sibling text nodes.
|
||||
#
|
||||
# With the +name+ argument, returns the next element with that name,
|
||||
# skipping other sibling elements.
|
||||
def next_element(element, name = nil)
|
||||
if siblings = element.parent.children
|
||||
found = false
|
||||
siblings.each do |node|
|
||||
if node.equal?(element)
|
||||
found = true
|
||||
elsif found && node.tag?
|
||||
return node if (name.nil? || node.name == name)
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
|
||||
# Creates a simple selector given the statement and array of
|
||||
# substitution values.
|
||||
#
|
||||
# Returns a hash with the values +tag_name+, +attributes+,
|
||||
# +pseudo+ (classes) and +negation+.
|
||||
#
|
||||
# Called the first time with +can_negate+ true to allow
|
||||
# negation. Called a second time with false since negation
|
||||
# cannot be negated.
|
||||
def simple_selector(statement, values, can_negate = true)
|
||||
tag_name = nil
|
||||
attributes = []
|
||||
pseudo = []
|
||||
negation = []
|
||||
|
||||
# Element name. (Note that in negation, this can come at
|
||||
# any order, but for simplicity we allow if only first).
|
||||
statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
|
||||
match.strip!
|
||||
tag_name = match.downcase unless match == "*"
|
||||
@source << match
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Get identifier, class, attribute name, pseudo or negation.
|
||||
while true
|
||||
# Element identifier.
|
||||
next if statement.sub!(/^#(\?|[\w\-]+)/) do |match|
|
||||
id = $1
|
||||
if id == "?"
|
||||
id = values.shift
|
||||
end
|
||||
@source << "##{id}"
|
||||
id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
|
||||
attributes << ["id", id]
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Class name.
|
||||
next if statement.sub!(/^\.([\w\-]+)/) do |match|
|
||||
class_name = $1
|
||||
@source << ".#{class_name}"
|
||||
class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
|
||||
attributes << ["class", class_name]
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Attribute value.
|
||||
next if statement.sub!(/^\[\s*([[:alpha:]][\w\-]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
|
||||
name, equality, value = $1, $2, $3
|
||||
if value == "?"
|
||||
value = values.shift
|
||||
else
|
||||
# Handle single and double quotes.
|
||||
value.strip!
|
||||
if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
|
||||
value = value[1..-2]
|
||||
end
|
||||
end
|
||||
@source << "[#{name}#{equality}'#{value}']"
|
||||
attributes << [name.downcase.strip, attribute_match(equality, value)]
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Root element only.
|
||||
next if statement.sub!(/^:root/) do |match|
|
||||
pseudo << lambda do |element|
|
||||
element.parent.nil? || !element.parent.tag?
|
||||
end
|
||||
@source << ":root"
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Nth-child including last and of-type.
|
||||
next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
|
||||
reverse = $1 == "last-"
|
||||
of_type = $2 == "of-type"
|
||||
@source << ":nth-#{$1}#{$2}("
|
||||
case $3
|
||||
when "odd"
|
||||
pseudo << nth_child(2, 1, of_type, reverse)
|
||||
@source << "odd)"
|
||||
when "even"
|
||||
pseudo << nth_child(2, 2, of_type, reverse)
|
||||
@source << "even)"
|
||||
when /^(\d+|\?)$/ # b only
|
||||
b = ($1 == "?" ? values.shift : $1).to_i
|
||||
pseudo << nth_child(0, b, of_type, reverse)
|
||||
@source << "#{b})"
|
||||
when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
|
||||
a = ($1 == "?" ? values.shift :
|
||||
$1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
|
||||
b = ($2 == "?" ? values.shift : $2).to_i
|
||||
pseudo << nth_child(a, b, of_type, reverse)
|
||||
@source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
|
||||
else
|
||||
raise ArgumentError, "Invalid nth-child #{match}"
|
||||
end
|
||||
"" # Remove
|
||||
end
|
||||
# First/last child (of type).
|
||||
next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match|
|
||||
reverse = $1 == "last"
|
||||
of_type = $2 == "of-type"
|
||||
pseudo << nth_child(0, 1, of_type, reverse)
|
||||
@source << ":#{$1}-#{$2}"
|
||||
"" # Remove
|
||||
end
|
||||
# Only child (of type).
|
||||
next if statement.sub!(/^:only-(child|of-type)/) do |match|
|
||||
of_type = $1 == "of-type"
|
||||
pseudo << only_child(of_type)
|
||||
@source << ":only-#{$1}"
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Empty: no child elements or meaningful content (whitespaces
|
||||
# are ignored).
|
||||
next if statement.sub!(/^:empty/) do |match|
|
||||
pseudo << lambda do |element|
|
||||
empty = true
|
||||
for child in element.children
|
||||
if child.tag? || !child.content.strip.empty?
|
||||
empty = false
|
||||
break
|
||||
end
|
||||
end
|
||||
empty
|
||||
end
|
||||
@source << ":empty"
|
||||
"" # Remove
|
||||
end
|
||||
# Content: match the text content of the element, stripping
|
||||
# leading and trailing spaces.
|
||||
next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match|
|
||||
content = $1
|
||||
if content == "?"
|
||||
content = values.shift
|
||||
elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
|
||||
content = content[1..-2]
|
||||
end
|
||||
@source << ":content('#{content}')"
|
||||
content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
|
||||
pseudo << lambda do |element|
|
||||
text = ""
|
||||
for child in element.children
|
||||
unless child.tag?
|
||||
text << child.content
|
||||
end
|
||||
end
|
||||
text.strip =~ content
|
||||
end
|
||||
"" # Remove
|
||||
end
|
||||
|
||||
# Negation. Create another simple selector to handle it.
|
||||
if statement.sub!(/^:not\(\s*/, "")
|
||||
raise ArgumentError, "Double negatives are not missing feature" unless can_negate
|
||||
@source << ":not("
|
||||
negation << simple_selector(statement, values, false)
|
||||
raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
|
||||
@source << ")"
|
||||
next
|
||||
end
|
||||
|
||||
# No match: moving on.
|
||||
break
|
||||
end
|
||||
|
||||
# Return hash. The keys are mapped to instance variables.
|
||||
{:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
|
||||
end
|
||||
|
||||
|
||||
# Create a regular expression to match an attribute value based
|
||||
# on the equality operator (=, ^=, |=, etc).
|
||||
def attribute_match(equality, value)
|
||||
regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
|
||||
case equality
|
||||
when "=" then
|
||||
# Match the attribute value in full
|
||||
Regexp.new("^#{regexp}$")
|
||||
when "~=" then
|
||||
# Match a space-separated word within the attribute value
|
||||
Regexp.new("(^|\s)#{regexp}($|\s)")
|
||||
when "^="
|
||||
# Match the beginning of the attribute value
|
||||
Regexp.new("^#{regexp}")
|
||||
when "$="
|
||||
# Match the end of the attribute value
|
||||
Regexp.new("#{regexp}$")
|
||||
when "*="
|
||||
# Match substring of the attribute value
|
||||
regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
|
||||
when "|=" then
|
||||
# Match the first space-separated item of the attribute value
|
||||
Regexp.new("^#{regexp}($|\s)")
|
||||
else
|
||||
raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
|
||||
# Match all attributes values (existence check)
|
||||
//
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns a lambda that can match an element against the nth-child
|
||||
# pseudo class, given the following arguments:
|
||||
# * +a+ -- Value of a part.
|
||||
# * +b+ -- Value of b part.
|
||||
# * +of_type+ -- True to test only elements of this type (of-type).
|
||||
# * +reverse+ -- True to count in reverse order (last-).
|
||||
def nth_child(a, b, of_type, reverse)
|
||||
# a = 0 means select at index b, if b = 0 nothing selected
|
||||
return lambda { |element| false } if a == 0 && b == 0
|
||||
# a < 0 and b < 0 will never match against an index
|
||||
return lambda { |element| false } if a < 0 && b < 0
|
||||
b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
|
||||
b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
|
||||
lambda do |element|
|
||||
# Element must be inside parent element.
|
||||
return false unless element.parent && element.parent.tag?
|
||||
index = 0
|
||||
# Get siblings, reverse if counting from last.
|
||||
siblings = element.parent.children
|
||||
siblings = siblings.reverse if reverse
|
||||
# Match element name if of-type, otherwise ignore name.
|
||||
name = of_type ? element.name : nil
|
||||
found = false
|
||||
for child in siblings
|
||||
# Skip text nodes/comments.
|
||||
if child.tag? && (name == nil || child.name == name)
|
||||
if a == 0
|
||||
# Shortcut when a == 0 no need to go past count
|
||||
if index == b
|
||||
found = child.equal?(element)
|
||||
break
|
||||
end
|
||||
elsif a < 0
|
||||
# Only look for first b elements
|
||||
break if index > b
|
||||
if child.equal?(element)
|
||||
found = (index % a) == 0
|
||||
break
|
||||
end
|
||||
else
|
||||
# Otherwise, break if child found and count == an+b
|
||||
if child.equal?(element)
|
||||
found = (index % a) == b
|
||||
break
|
||||
end
|
||||
end
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
found
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Creates a only child lambda. Pass +of-type+ to only look at
|
||||
# elements of its type.
|
||||
def only_child(of_type)
|
||||
lambda do |element|
|
||||
# Element must be inside parent element.
|
||||
return false unless element.parent && element.parent.tag?
|
||||
name = of_type ? element.name : nil
|
||||
other = false
|
||||
for child in element.parent.children
|
||||
# Skip text nodes/comments.
|
||||
if child.tag? && (name == nil || child.name == name)
|
||||
unless child.equal?(element)
|
||||
other = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
!other
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Called to create a dependent selector (sibling, descendant, etc).
|
||||
# Passes the remainder of the statement that will be reduced to zero
|
||||
# eventually, and array of substitution values.
|
||||
#
|
||||
# This method is called from four places, so it helps to put it here
|
||||
# for resue. The only logic deals with the need to detect comma
|
||||
# separators (alternate) and apply them to the selector group of the
|
||||
# top selector.
|
||||
def next_selector(statement, values)
|
||||
second = Selector.new(statement, values)
|
||||
# If there are alternate selectors, we group them in the top selector.
|
||||
if alternates = second.instance_variable_get(:@alternates)
|
||||
second.instance_variable_set(:@alternates, nil)
|
||||
(@alternates ||= []).concat alternates
|
||||
end
|
||||
second
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
# See HTML::Selector.new
|
||||
def self.selector(statement, *values)
|
||||
Selector.new(statement, *values)
|
||||
end
|
||||
|
||||
|
||||
class Tag
|
||||
|
||||
def select(selector, *values)
|
||||
selector = HTML::Selector.new(selector, values)
|
||||
selector.select(self)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
490
actionpack/test/controller/assert_select_test.rb
Normal file
490
actionpack/test/controller/assert_select_test.rb
Normal file
@@ -0,0 +1,490 @@
|
||||
#--
|
||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
||||
# Under MIT and/or CC By license.
|
||||
#++
|
||||
|
||||
require File.dirname(__FILE__) + '/../abstract_unit'
|
||||
require File.dirname(__FILE__) + '/fake_controllers'
|
||||
require "action_mailer"
|
||||
|
||||
class AssertSelectTest < Test::Unit::TestCase
|
||||
class AssertSelectController < ActionController::Base
|
||||
def response_with=(content)
|
||||
@content = content
|
||||
end
|
||||
|
||||
def response_with(&block)
|
||||
@update = block
|
||||
end
|
||||
|
||||
def html()
|
||||
render :text=>@content, :layout=>false, :content_type=>Mime::HTML
|
||||
@content = nil
|
||||
end
|
||||
|
||||
def rjs()
|
||||
render :update do |page|
|
||||
@update.call page
|
||||
end
|
||||
@update = nil
|
||||
end
|
||||
|
||||
def xml()
|
||||
render :text=>@content, :layout=>false, :content_type=>Mime::XML
|
||||
@content = nil
|
||||
end
|
||||
|
||||
def rescue_action(e)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
class AssertSelectMailer < ActionMailer::Base
|
||||
def test(html)
|
||||
recipients "test <test@test.host>"
|
||||
from "test@test.host"
|
||||
subject "Test e-mail"
|
||||
part :content_type=>"text/html", :body=>html
|
||||
end
|
||||
end
|
||||
|
||||
AssertionFailedError = Test::Unit::AssertionFailedError
|
||||
|
||||
def setup
|
||||
@controller = AssertSelectController.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
ActionMailer::Base.delivery_method = :test
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
ActionMailer::Base.deliveries = []
|
||||
end
|
||||
|
||||
|
||||
def teardown
|
||||
ActionMailer::Base.deliveries.clear
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Test assert select.
|
||||
#
|
||||
|
||||
def test_assert_select
|
||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
||||
assert_select "div", 2
|
||||
assert_raises(AssertionFailedError) { assert_select "div", 3 }
|
||||
assert_raises(AssertionFailedError){ assert_select "p" }
|
||||
end
|
||||
|
||||
|
||||
def test_equality_true_false
|
||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
||||
assert_nothing_raised { assert_select "div" }
|
||||
assert_raises(AssertionFailedError) { assert_select "p" }
|
||||
assert_nothing_raised { assert_select "div", true }
|
||||
assert_raises(AssertionFailedError) { assert_select "p", true }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", false }
|
||||
assert_nothing_raised { assert_select "p", false }
|
||||
end
|
||||
|
||||
|
||||
def test_equality_string_and_regexp
|
||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
||||
assert_nothing_raised { assert_select "div", "foo" }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", "bar" }
|
||||
assert_nothing_raised { assert_select "div", :text=>"foo" }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :text=>"bar" }
|
||||
assert_nothing_raised { assert_select "div", /(foo|bar)/ }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", /foobar/ }
|
||||
assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :text=>/foobar/ }
|
||||
assert_raises(AssertionFailedError) { assert_select "p", :text=>/foobar/ }
|
||||
end
|
||||
|
||||
|
||||
def test_equality_of_html
|
||||
render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>}
|
||||
text = "\"This is not a big problem,\" he said."
|
||||
html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said."
|
||||
assert_nothing_raised { assert_select "p", text }
|
||||
assert_raises(AssertionFailedError) { assert_select "p", html }
|
||||
assert_nothing_raised { assert_select "p", :html=>html }
|
||||
assert_raises(AssertionFailedError) { assert_select "p", :html=>text }
|
||||
# No stripping for pre.
|
||||
render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>}
|
||||
text = "\n\"This is not a big problem,\" he said.\n"
|
||||
html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n"
|
||||
assert_nothing_raised { assert_select "pre", text }
|
||||
assert_raises(AssertionFailedError) { assert_select "pre", html }
|
||||
assert_nothing_raised { assert_select "pre", :html=>html }
|
||||
assert_raises(AssertionFailedError) { assert_select "pre", :html=>text }
|
||||
end
|
||||
|
||||
|
||||
def test_equality_of_instances
|
||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
||||
assert_nothing_raised { assert_select "div", 2 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", 3 }
|
||||
assert_nothing_raised { assert_select "div", 1..2 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", 3..4 }
|
||||
assert_nothing_raised { assert_select "div", :count=>2 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :count=>3 }
|
||||
assert_nothing_raised { assert_select "div", :minimum=>1 }
|
||||
assert_nothing_raised { assert_select "div", :minimum=>2 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :minimum=>3 }
|
||||
assert_nothing_raised { assert_select "div", :maximum=>2 }
|
||||
assert_nothing_raised { assert_select "div", :maximum=>3 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :maximum=>1 }
|
||||
assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
|
||||
assert_raises(AssertionFailedError) { assert_select "div", :minimum=>3, :maximum=>4 }
|
||||
end
|
||||
|
||||
|
||||
def test_substitution_values
|
||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
||||
assert_select "div#?", /\d+/ do |elements|
|
||||
assert_equal 2, elements.size
|
||||
end
|
||||
assert_select "div" do
|
||||
assert_select "div#?", /\d+/ do |elements|
|
||||
assert_equal 2, elements.size
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_nested_assert_select
|
||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
||||
assert_select "div" do |elements|
|
||||
assert_equal 2, elements.size
|
||||
assert_select elements[0], "#1"
|
||||
assert_select elements[1], "#2"
|
||||
end
|
||||
assert_select "div" do
|
||||
assert_select "div" do |elements|
|
||||
assert_equal 2, elements.size
|
||||
# Testing in a group is one thing
|
||||
assert_select "#1,#2"
|
||||
# Testing individually is another.
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
assert_select "#3", false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_assert_select_from_rjs
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_select "div" do |elements|
|
||||
assert elements.size == 2
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
end
|
||||
assert_select "div#?", /\d+/ do |elements|
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
end
|
||||
# With multiple results.
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_select "div" do |elements|
|
||||
assert elements.size == 2
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Test css_select.
|
||||
#
|
||||
|
||||
|
||||
def test_css_select
|
||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
||||
assert 2, css_select("div").size
|
||||
assert 0, css_select("p").size
|
||||
end
|
||||
|
||||
|
||||
def test_nested_css_select
|
||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
||||
assert_select "div#?", /\d+/ do |elements|
|
||||
assert_equal 1, css_select(elements[0], "div").size
|
||||
assert_equal 1, css_select(elements[1], "div").size
|
||||
end
|
||||
assert_select "div" do
|
||||
assert_equal 2, css_select("div").size
|
||||
css_select("div").each do |element|
|
||||
# Testing as a group is one thing
|
||||
assert !css_select("#1,#2").empty?
|
||||
# Testing individually is another
|
||||
assert !css_select("#1").empty?
|
||||
assert !css_select("#2").empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_css_select_from_rjs
|
||||
# With one result.
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_equal 2, css_select("div").size
|
||||
assert_equal 1, css_select("#1").size
|
||||
assert_equal 1, css_select("#2").size
|
||||
# With multiple results.
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_equal 2, css_select("div").size
|
||||
assert_equal 1, css_select("#1").size
|
||||
assert_equal 1, css_select("#2").size
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Test assert_select_rjs.
|
||||
#
|
||||
|
||||
|
||||
def test_assert_select_rjs
|
||||
# Test that we can pick up all statements in the result.
|
||||
render_rjs do |page|
|
||||
page.replace "test", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
|
||||
end
|
||||
found = false
|
||||
assert_select_rjs do
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
assert_select "#3"
|
||||
found = true
|
||||
end
|
||||
assert found
|
||||
# Test that we fail if there is nothing to pick.
|
||||
render_rjs do |page|
|
||||
end
|
||||
assert_raises(AssertionFailedError) { assert_select_rjs }
|
||||
end
|
||||
|
||||
|
||||
def test_assert_select_rjs_with_id
|
||||
# Test that we can pick up all statements in the result.
|
||||
render_rjs do |page|
|
||||
page.replace "test1", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
|
||||
end
|
||||
assert_select_rjs "test1" do
|
||||
assert_select "div", 1
|
||||
assert_select "#1"
|
||||
end
|
||||
assert_select_rjs "test2" do
|
||||
assert_select "div", 1
|
||||
assert_select "#2"
|
||||
end
|
||||
assert_select_rjs "test3" do
|
||||
assert_select "div", 1
|
||||
assert_select "#3"
|
||||
end
|
||||
assert_raises(AssertionFailedError) { assert_select_rjs "test4" }
|
||||
end
|
||||
|
||||
|
||||
def test_assert_select_rjs_for_replace
|
||||
render_rjs do |page|
|
||||
page.replace "test1", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
|
||||
end
|
||||
# Replace.
|
||||
assert_select_rjs :replace do
|
||||
assert_select "div", 1
|
||||
assert_select "#1"
|
||||
end
|
||||
assert_select_rjs :replace, "test1" do
|
||||
assert_select "div", 1
|
||||
assert_select "#1"
|
||||
end
|
||||
assert_raises(AssertionFailedError) { assert_select_rjs :replace, "test2" }
|
||||
# Replace HTML.
|
||||
assert_select_rjs :replace_html do
|
||||
assert_select "div", 1
|
||||
assert_select "#2"
|
||||
end
|
||||
assert_select_rjs :replace_html, "test2" do
|
||||
assert_select "div", 1
|
||||
assert_select "#2"
|
||||
end
|
||||
assert_raises(AssertionFailedError) { assert_select_rjs :replace_html, "test1" }
|
||||
end
|
||||
|
||||
|
||||
def test_assert_select_rjs_for_insert
|
||||
render_rjs do |page|
|
||||
page.replace "test1", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
page.insert_html :top, "test3", "<div id=\"3\">foo</div>"
|
||||
end
|
||||
# Non-positioned.
|
||||
assert_select_rjs :insert_html do
|
||||
assert_select "div", 1
|
||||
assert_select "#3"
|
||||
end
|
||||
assert_select_rjs :insert_html, "test3" do
|
||||
assert_select "div", 1
|
||||
assert_select "#3"
|
||||
end
|
||||
assert_raises(AssertionFailedError) { assert_select_rjs :insert_html, "test1" }
|
||||
# Positioned.
|
||||
render_rjs do |page|
|
||||
page.insert_html :top, "test1", "<div id=\"1\">foo</div>"
|
||||
page.insert_html :bottom, "test2", "<div id=\"2\">foo</div>"
|
||||
page.insert_html :before, "test3", "<div id=\"3\">foo</div>"
|
||||
page.insert_html :after, "test4", "<div id=\"4\">foo</div>"
|
||||
end
|
||||
assert_select_rjs :insert, :top do
|
||||
assert_select "div", 1
|
||||
assert_select "#1"
|
||||
end
|
||||
assert_select_rjs :insert, :bottom do
|
||||
assert_select "div", 1
|
||||
assert_select "#2"
|
||||
end
|
||||
assert_select_rjs :insert, :before do
|
||||
assert_select "div", 1
|
||||
assert_select "#3"
|
||||
end
|
||||
assert_select_rjs :insert, :after do
|
||||
assert_select "div", 1
|
||||
assert_select "#4"
|
||||
end
|
||||
assert_select_rjs :insert_html do
|
||||
assert_select "div", 4
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_nested_assert_select_rjs
|
||||
# Simple selection from a single result.
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>\n<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_select_rjs "test" do |elements|
|
||||
assert_equal 2, elements.size
|
||||
assert_select "#1"
|
||||
assert_select "#2"
|
||||
end
|
||||
# Deal with two results.
|
||||
render_rjs do |page|
|
||||
page.replace_html "test", "<div id=\"1\">foo</div>"
|
||||
page.replace_html "test2", "<div id=\"2\">foo</div>"
|
||||
end
|
||||
assert_select_rjs "test" do |elements|
|
||||
assert_equal 1, elements.size
|
||||
assert_select "#1"
|
||||
end
|
||||
assert_select_rjs "test2" do |elements|
|
||||
assert_equal 1, elements.size
|
||||
assert_select "#2"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def test_feed_item_encoded
|
||||
render_xml <<-EOF
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<item>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<p>Test 1</p>
|
||||
]]>
|
||||
</description>
|
||||
</item>
|
||||
<item>
|
||||
<description>
|
||||
<![CDATA[
|
||||
<p>Test 2</p>
|
||||
]]>
|
||||
</description>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
EOF
|
||||
assert_select "channel item description" do
|
||||
# Test element regardless of wrapper.
|
||||
assert_select_encoded do
|
||||
assert_select "p", :count=>2, :text=>/Test/
|
||||
end
|
||||
# Test through encoded wrapper.
|
||||
assert_select_encoded do
|
||||
assert_select "encoded p", :count=>2, :text=>/Test/
|
||||
end
|
||||
# Use :root instead (recommended)
|
||||
assert_select_encoded do
|
||||
assert_select ":root p", :count=>2, :text=>/Test/
|
||||
end
|
||||
# Test individually.
|
||||
assert_select "description" do |elements|
|
||||
assert_select_encoded elements[0] do
|
||||
assert_select "p", "Test 1"
|
||||
end
|
||||
assert_select_encoded elements[1] do
|
||||
assert_select "p", "Test 2"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Test that we only un-encode element itself.
|
||||
assert_select "channel item" do
|
||||
assert_select_encoded do
|
||||
assert_select "p", 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Test assert_select_email
|
||||
#
|
||||
|
||||
def test_assert_select_email
|
||||
assert_raises(AssertionFailedError) { assert_select_email {} }
|
||||
AssertSelectMailer.deliver_test "<div><p>foo</p><p>bar</p></div>"
|
||||
assert_select_email do
|
||||
assert_select "div:root" do
|
||||
assert_select "p:first-child", "foo"
|
||||
assert_select "p:last-child", "bar"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
def render_html(html)
|
||||
@controller.response_with = html
|
||||
get :html
|
||||
end
|
||||
|
||||
def render_rjs(&block)
|
||||
@controller.response_with &block
|
||||
get :rjs
|
||||
end
|
||||
|
||||
def render_xml(xml)
|
||||
@controller.response_with = xml
|
||||
get :xml
|
||||
end
|
||||
end
|
||||
628
actionpack/test/controller/selector_test.rb
Normal file
628
actionpack/test/controller/selector_test.rb
Normal file
@@ -0,0 +1,628 @@
|
||||
#--
|
||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
||||
# Under MIT and/or CC By license.
|
||||
#++
|
||||
|
||||
require File.dirname(__FILE__) + '/../abstract_unit'
|
||||
require File.dirname(__FILE__) + '/fake_controllers'
|
||||
|
||||
class SelectorTest < Test::Unit::TestCase
|
||||
#
|
||||
# Basic selector: element, id, class, attributes.
|
||||
#
|
||||
|
||||
def test_element
|
||||
parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
|
||||
# Match element by name.
|
||||
select("div")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
# Not case sensitive.
|
||||
select("DIV")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
# Universal match (all elements).
|
||||
select("*")
|
||||
assert_equal 3, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal nil, @matches[1].attributes["id"]
|
||||
assert_equal "2", @matches[2].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_identifier
|
||||
parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
|
||||
# Match element by ID.
|
||||
select("div#1")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
# Match element by ID, substitute value.
|
||||
select("div#?", 2)
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Element name does not match ID.
|
||||
select("p#?", 2)
|
||||
assert_equal 0, @matches.size
|
||||
# Use regular expression.
|
||||
select("#?", /\d/)
|
||||
assert_equal 2, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_class_name
|
||||
parse(%Q{<div id="1" class=" foo "></div><p id="2" class=" foo bar "></p><div id="3" class="bar"></div>})
|
||||
# Match element with specified class.
|
||||
select("div.foo")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
# Match any element with specified class.
|
||||
select("*.foo")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
# Match elements with other class.
|
||||
select("*.bar")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# Match only element with both class names.
|
||||
select("*.bar.foo")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_attribute
|
||||
parse(%Q{<div id="1"></div><p id="2" title="" bar="foo"></p><div id="3" title="foo"></div>})
|
||||
# Match element with attribute.
|
||||
select("div[title]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
# Match any element with attribute.
|
||||
select("*[title]")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# Match alement with attribute value.
|
||||
select("*[title=foo]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
# Match alement with attribute and attribute value.
|
||||
select("[bar=foo][title]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Not case sensitive.
|
||||
select("[BAR=foo][TiTle]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_attribute_quoted
|
||||
parse(%Q{<div id="1" title="foo"></div><div id="2" title="bar"></div><div id="3" title=" bar "></div>})
|
||||
# Match without quotes.
|
||||
select("[title = bar]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Match with single quotes.
|
||||
select("[title = 'bar' ]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Match with double quotes.
|
||||
select("[title = \"bar\" ]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Match with spaces.
|
||||
select("[title = \" bar \" ]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_attribute_equality
|
||||
parse(%Q{<div id="1" title="foo bar"></div><div id="2" title="barbaz"></div>})
|
||||
# Match (fail) complete value.
|
||||
select("[title=bar]")
|
||||
assert_equal 0, @matches.size
|
||||
# Match space-separate word.
|
||||
select("[title~=foo]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
select("[title~=bar]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
# Match beginning of value.
|
||||
select("[title^=ba]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Match end of value.
|
||||
select("[title$=ar]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
# Match text in value.
|
||||
select("[title*=bar]")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
# Match first space separated word.
|
||||
select("[title|=foo]")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
select("[title|=bar]")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Selector composition: groups, sibling, children
|
||||
#
|
||||
|
||||
|
||||
def test_selector_group
|
||||
parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
|
||||
# Simple group selector.
|
||||
select("h1,h3")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
select("h1 , h3")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# Complex group selector.
|
||||
parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
|
||||
select("h1 a, h3 a")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "foo", @matches[0].attributes["href"]
|
||||
assert_equal "baz", @matches[1].attributes["href"]
|
||||
# And now for the three selector challange.
|
||||
parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
|
||||
select("h1 a, h2 a, h3 a")
|
||||
assert_equal 3, @matches.size
|
||||
assert_equal "foo", @matches[0].attributes["href"]
|
||||
assert_equal "bar", @matches[1].attributes["href"]
|
||||
assert_equal "baz", @matches[2].attributes["href"]
|
||||
end
|
||||
|
||||
|
||||
def test_sibling_selector
|
||||
parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
|
||||
# Test next sibling.
|
||||
select("h1+*")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
select("h1+h2")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
select("h1+h3")
|
||||
assert_equal 0, @matches.size
|
||||
select("*+h3")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
# Test any sibling.
|
||||
select("h1~*")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
select("h2~*")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_children_selector
|
||||
parse(%Q{<div><p id="1"><span id="2"></span></p></div><div><p id="3"><span id="4" class="foo"></span></p></div>})
|
||||
# Test child selector.
|
||||
select("div>p")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
select("div>span")
|
||||
assert_equal 0, @matches.size
|
||||
select("div>p#3")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
select("div>p>span")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
# Test descendant selector.
|
||||
select("div p")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
select("div span")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
select("div *#3")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
select("div *#4")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "4", @matches[0].attributes["id"]
|
||||
# This is here because it failed before when whitespaces
|
||||
# were not properly stripped.
|
||||
select("div .foo")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "4", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Pseudo selectors: root, nth-child, empty, content, etc
|
||||
#
|
||||
|
||||
|
||||
def test_root_selector
|
||||
parse(%Q{<div id="1"><div id="2"></div></div>})
|
||||
# Can only find element if it's root.
|
||||
select(":root")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
select("#1:root")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
select("#2:root")
|
||||
assert_equal 0, @matches.size
|
||||
# Opposite for nth-child.
|
||||
select("#1:nth-child(1)")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_odd_even
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Test odd nth children.
|
||||
select("tr:nth-child(odd)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# Test even nth children.
|
||||
select("tr:nth-child(even)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_a_is_zero
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Test the third child.
|
||||
select("tr:nth-child(0n+3)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
# Same but an can be omitted when zero.
|
||||
select("tr:nth-child(3)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
# Second element (but not every second element).
|
||||
select("tr:nth-child(0n+2)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Before first and past last returns nothing.:
|
||||
assert_raises(ArgumentError) { select("tr:nth-child(-1)") }
|
||||
select("tr:nth-child(0)")
|
||||
assert_equal 0, @matches.size
|
||||
select("tr:nth-child(5)")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_a_is_one
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# a is group of one, pick every element in group.
|
||||
select("tr:nth-child(1n+0)")
|
||||
assert_equal 4, @matches.size
|
||||
# Same but a can be omitted when one.
|
||||
select("tr:nth-child(n+0)")
|
||||
assert_equal 4, @matches.size
|
||||
# Same but b can be omitted when zero.
|
||||
select("tr:nth-child(n)")
|
||||
assert_equal 4, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_b_is_zero
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# If b is zero, pick the n-th element (here each one).
|
||||
select("tr:nth-child(n+0)")
|
||||
assert_equal 4, @matches.size
|
||||
# If b is zero, pick the n-th element (here every second).
|
||||
select("tr:nth-child(2n+0)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# If a and b are both zero, no element selected.
|
||||
select("tr:nth-child(0n+0)")
|
||||
assert_equal 0, @matches.size
|
||||
select("tr:nth-child(0)")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_a_is_negative
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Since a is -1, picks the first three elements.
|
||||
select("tr:nth-child(-n+3)")
|
||||
assert_equal 3, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
assert_equal "3", @matches[2].attributes["id"]
|
||||
# Since a is -2, picks the first in every second of first four elements.
|
||||
select("tr:nth-child(-2n+3)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
# Since a is -2, picks the first in every second of first three elements.
|
||||
select("tr:nth-child(-2n+2)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_b_is_negative
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Select last of four.
|
||||
select("tr:nth-child(4n-1)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "4", @matches[0].attributes["id"]
|
||||
# Select first of four.
|
||||
select("tr:nth-child(4n-4)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
# Select last of every second.
|
||||
select("tr:nth-child(2n-1)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
# Select nothing since an+b always < 0
|
||||
select("tr:nth-child(-1n-1)")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_child_substitution_values
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Test with ?n?.
|
||||
select("tr:nth-child(?n?)", 2, 1)
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "3", @matches[1].attributes["id"]
|
||||
select("tr:nth-child(?n?)", 2, 2)
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
select("tr:nth-child(?n?)", 4, 2)
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
# Test with ? (b only).
|
||||
select("tr:nth-child(?)", 3)
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
select("tr:nth-child(?)", 5)
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_nth_last_child
|
||||
parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# Last two elements.
|
||||
select("tr:nth-last-child(-n+2)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "3", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
# All old elements counting from last one.
|
||||
select("tr:nth-last-child(odd)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_nth_of_type
|
||||
parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# First two elements.
|
||||
select("tr:nth-of-type(-n+2)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
# All old elements counting from last one.
|
||||
select("tr:nth-last-of-type(odd)")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
assert_equal "4", @matches[1].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_first_and_last
|
||||
parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
|
||||
# First child.
|
||||
select("tr:first-child")
|
||||
assert_equal 0, @matches.size
|
||||
select(":first-child")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "thead", @matches[0].name
|
||||
# First of type.
|
||||
select("tr:first-of-type")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
select("thead:first-of-type")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "thead", @matches[0].name
|
||||
select("div:first-of-type")
|
||||
assert_equal 0, @matches.size
|
||||
# Last child.
|
||||
select("tr:last-child")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "4", @matches[0].attributes["id"]
|
||||
# Last of type.
|
||||
select("tr:last-of-type")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "4", @matches[0].attributes["id"]
|
||||
select("thead:last-of-type")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "thead", @matches[0].name
|
||||
select("div:last-of-type")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_first_and_last
|
||||
# Only child.
|
||||
parse(%Q{<table><tr></tr></table>})
|
||||
select("table:only-child")
|
||||
assert_equal 0, @matches.size
|
||||
select("tr:only-child")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "tr", @matches[0].name
|
||||
parse(%Q{<table><tr></tr><tr></tr></table>})
|
||||
select("tr:only-child")
|
||||
assert_equal 0, @matches.size
|
||||
# Only of type.
|
||||
parse(%Q{<table><thead></thead><tr></tr><tr></tr></table>})
|
||||
select("thead:only-of-type")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "thead", @matches[0].name
|
||||
select("td:only-of-type")
|
||||
assert_equal 0, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_empty
|
||||
parse(%Q{<table><tr></tr></table>})
|
||||
select("table:empty")
|
||||
assert_equal 0, @matches.size
|
||||
select("tr:empty")
|
||||
assert_equal 1, @matches.size
|
||||
parse(%Q{<div> </div>})
|
||||
select("div:empty")
|
||||
assert_equal 1, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_content
|
||||
parse(%Q{<div> </div>})
|
||||
select("div:content()")
|
||||
assert_equal 1, @matches.size
|
||||
parse(%Q{<div>something </div>})
|
||||
select("div:content()")
|
||||
assert_equal 0, @matches.size
|
||||
select("div:content(something)")
|
||||
assert_equal 1, @matches.size
|
||||
select("div:content( 'something' )")
|
||||
assert_equal 1, @matches.size
|
||||
select("div:content( \"something\" )")
|
||||
assert_equal 1, @matches.size
|
||||
select("div:content(?)", "something")
|
||||
assert_equal 1, @matches.size
|
||||
select("div:content(?)", /something/)
|
||||
assert_equal 1, @matches.size
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Test negation.
|
||||
#
|
||||
|
||||
|
||||
def test_element_negation
|
||||
parse(%Q{<p></p><div></div>})
|
||||
select("*")
|
||||
assert_equal 2, @matches.size
|
||||
select("*:not(p)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "div", @matches[0].name
|
||||
select("*:not(div)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "p", @matches[0].name
|
||||
select("*:not(span)")
|
||||
assert_equal 2, @matches.size
|
||||
end
|
||||
|
||||
|
||||
def test_id_negation
|
||||
parse(%Q{<p id="1"></p><p id="2"></p>})
|
||||
select("p")
|
||||
assert_equal 2, @matches.size
|
||||
select(":not(#1)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
select(":not(#2)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_class_name_negation
|
||||
parse(%Q{<p class="foo"></p><p class="bar"></p>})
|
||||
select("p")
|
||||
assert_equal 2, @matches.size
|
||||
select(":not(.foo)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "bar", @matches[0].attributes["class"]
|
||||
select(":not(.bar)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "foo", @matches[0].attributes["class"]
|
||||
end
|
||||
|
||||
|
||||
def test_attribute_negation
|
||||
parse(%Q{<p title="foo"></p><p title="bar"></p>})
|
||||
select("p")
|
||||
assert_equal 2, @matches.size
|
||||
select(":not([title=foo])")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "bar", @matches[0].attributes["title"]
|
||||
select(":not([title=bar])")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "foo", @matches[0].attributes["title"]
|
||||
end
|
||||
|
||||
|
||||
def test_pseudo_class_negation
|
||||
parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
|
||||
select("p")
|
||||
assert_equal 2, @matches.size
|
||||
select("p:not(:first-child)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
select("p:not(:nth-child(2))")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_negation_details
|
||||
parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>})
|
||||
assert_raises(ArgumentError) { select(":not(") }
|
||||
assert_raises(ArgumentError) { select(":not(:not())") }
|
||||
select("p:not(#1):not(#3)")
|
||||
assert_equal 1, @matches.size
|
||||
assert_equal "2", @matches[0].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
def test_select_from_element
|
||||
parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
|
||||
select("div")
|
||||
@matches = @matches[0].select("p")
|
||||
assert_equal 2, @matches.size
|
||||
assert_equal "1", @matches[0].attributes["id"]
|
||||
assert_equal "2", @matches[1].attributes["id"]
|
||||
end
|
||||
|
||||
|
||||
protected
|
||||
|
||||
def parse(html)
|
||||
@html = HTML::Document.new(html).root
|
||||
end
|
||||
|
||||
def select(*selector)
|
||||
@matches = HTML.selector(*selector).select(@html)
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user