mirror of https://github.com/rails/rails
commit
cdc00aba62
3
Gemfile
3
Gemfile
|
@ -12,6 +12,9 @@ gem 'rack-cache', '~> 1.2'
|
||||||
gem 'jquery-rails', '~> 3.1.0'
|
gem 'jquery-rails', '~> 3.1.0'
|
||||||
gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'
|
gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'
|
||||||
gem 'coffee-rails', '~> 4.0.0'
|
gem 'coffee-rails', '~> 4.0.0'
|
||||||
|
gem 'rails-html-sanitizer', github: 'rails/rails-html-sanitizer'
|
||||||
|
#temporary gem until a new version of loofah is released
|
||||||
|
gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub'
|
||||||
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
|
gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
|
||||||
gem 'i18n', github: 'svenfuchs/i18n', branch: 'master'
|
gem 'i18n', github: 'svenfuchs/i18n', branch: 'master'
|
||||||
|
|
||||||
|
|
|
@ -23,4 +23,5 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency 'actionview', version
|
s.add_dependency 'actionview', version
|
||||||
|
|
||||||
s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
|
s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
|
||||||
|
s.add_dependency 'rails-dom-testing'
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
require 'active_support/test_case'
|
require 'active_support/test_case'
|
||||||
|
require 'rails-dom-testing'
|
||||||
|
|
||||||
module ActionMailer
|
module ActionMailer
|
||||||
class NonInferrableMailerError < ::StandardError
|
class NonInferrableMailerError < ::StandardError
|
||||||
|
@ -15,6 +16,7 @@ module ActionMailer
|
||||||
|
|
||||||
include ActiveSupport::Testing::ConstantLookup
|
include ActiveSupport::Testing::ConstantLookup
|
||||||
include TestHelper
|
include TestHelper
|
||||||
|
include Rails::Dom::Testing::Assertions::SelectorAssertions
|
||||||
|
|
||||||
included do
|
included do
|
||||||
class_attribute :_mailer_class
|
class_attribute :_mailer_class
|
||||||
|
@ -55,14 +57,13 @@ module ActionMailer
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def initialize_test_deliveries
|
def initialize_test_deliveries
|
||||||
@old_delivery_method = ActionMailer::Base.delivery_method
|
set_delivery_method :test
|
||||||
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
|
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
|
||||||
ActionMailer::Base.delivery_method = :test
|
|
||||||
ActionMailer::Base.perform_deliveries = true
|
ActionMailer::Base.perform_deliveries = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def restore_test_deliveries
|
def restore_test_deliveries
|
||||||
ActionMailer::Base.delivery_method = @old_delivery_method
|
restore_delivery_method
|
||||||
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
|
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
|
||||||
ActionMailer::Base.deliveries.clear
|
ActionMailer::Base.deliveries.clear
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,7 +26,7 @@ I18n.enforce_available_locales = false
|
||||||
FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
|
FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
|
||||||
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
|
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
|
||||||
|
|
||||||
class Rails
|
module Rails
|
||||||
def self.root
|
def self.root
|
||||||
File.expand_path('../', File.dirname(__FILE__))
|
File.expand_path('../', File.dirname(__FILE__))
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
require 'abstract_unit'
|
||||||
|
|
||||||
|
class AssertSelectEmailTest < ActionMailer::TestCase
|
||||||
|
class AssertSelectMailer < ActionMailer::Base
|
||||||
|
def test(html)
|
||||||
|
mail body: html, content_type: "text/html",
|
||||||
|
subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class AssertMultipartSelectMailer < ActionMailer::Base
|
||||||
|
def test(options)
|
||||||
|
mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
|
||||||
|
format.text { render text: options[:text] }
|
||||||
|
format.html { render text: options[:html] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Test assert_select_email
|
||||||
|
#
|
||||||
|
|
||||||
|
def test_assert_select_email
|
||||||
|
assert_raise ActiveSupport::TestCase::Assertion do
|
||||||
|
assert_select_email {}
|
||||||
|
end
|
||||||
|
|
||||||
|
AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
|
||||||
|
assert_select_email do
|
||||||
|
assert_select "div:root" do
|
||||||
|
assert_select "p:first-child", "foo"
|
||||||
|
assert_select "p:last-child", "bar"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assert_select_email_multipart
|
||||||
|
AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver
|
||||||
|
assert_select_email do
|
||||||
|
assert_select "div:root" do
|
||||||
|
assert_select "p:first-child", "foo"
|
||||||
|
assert_select "p:last-child", "bar"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,3 +1,7 @@
|
||||||
|
* Deleted the deprecated TagAssertions.
|
||||||
|
|
||||||
|
*Kasper Timm Hansen*
|
||||||
|
|
||||||
* Use the Active Support JSON encoder for cookie jars using the `:json` or
|
* Use the Active Support JSON encoder for cookie jars using the `:json` or
|
||||||
`:hybrid` serializer. This allows you to serialize custom Ruby objects into
|
`:hybrid` serializer. This allows you to serialize custom Ruby objects into
|
||||||
cookies by defining the `#as_json` hook on such objects.
|
cookies by defining the `#as_json` hook on such objects.
|
||||||
|
|
|
@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
||||||
|
|
||||||
s.add_dependency 'rack', '~> 1.6.0.alpha'
|
s.add_dependency 'rack', '~> 1.6.0.alpha'
|
||||||
s.add_dependency 'rack-test', '~> 0.6.2'
|
s.add_dependency 'rack-test', '~> 0.6.2'
|
||||||
|
s.add_dependency 'rails-deprecated_sanitizer'
|
||||||
s.add_dependency 'actionview', version
|
s.add_dependency 'actionview', version
|
||||||
|
|
||||||
s.add_development_dependency 'activemodel', version
|
s.add_development_dependency 'activemodel', version
|
||||||
|
|
|
@ -3,6 +3,8 @@ require 'active_support/core_ext/object/to_query'
|
||||||
require 'active_support/core_ext/module/anonymous'
|
require 'active_support/core_ext/module/anonymous'
|
||||||
require 'active_support/core_ext/hash/keys'
|
require 'active_support/core_ext/hash/keys'
|
||||||
|
|
||||||
|
require 'rails-dom-testing'
|
||||||
|
|
||||||
module ActionController
|
module ActionController
|
||||||
module TemplateAssertions
|
module TemplateAssertions
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
@ -442,6 +444,7 @@ module ActionController
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include ActionDispatch::TestProcess
|
include ActionDispatch::TestProcess
|
||||||
include ActiveSupport::Testing::ConstantLookup
|
include ActiveSupport::Testing::ConstantLookup
|
||||||
|
include Rails::Dom::Testing::Assertions
|
||||||
|
|
||||||
attr_reader :response, :request
|
attr_reader :response, :request
|
||||||
|
|
||||||
|
@ -684,6 +687,11 @@ module ActionController
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def document_root_element
|
||||||
|
html_document.root
|
||||||
|
end
|
||||||
|
|
||||||
def check_required_ivars
|
def check_required_ivars
|
||||||
# Sanity check for required instance variables so we can give an
|
# Sanity check for required instance variables so we can give an
|
||||||
# understandable error message.
|
# understandable error message.
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
|
require 'rails-dom-testing'
|
||||||
|
|
||||||
module ActionDispatch
|
module ActionDispatch
|
||||||
module Assertions
|
module Assertions
|
||||||
autoload :DomAssertions, 'action_dispatch/testing/assertions/dom'
|
|
||||||
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
|
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
|
||||||
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
|
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
|
||||||
autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector'
|
|
||||||
autoload :TagAssertions, 'action_dispatch/testing/assertions/tag'
|
|
||||||
|
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include DomAssertions
|
|
||||||
include ResponseAssertions
|
include ResponseAssertions
|
||||||
include RoutingAssertions
|
include RoutingAssertions
|
||||||
include SelectorAssertions
|
include Rails::Dom::Testing::Assertions
|
||||||
include TagAssertions
|
|
||||||
|
def html_document
|
||||||
|
@html_document ||= if @response.content_type =~ /xml$/
|
||||||
|
Nokogiri::XML::Document.parse(@response.body)
|
||||||
|
else
|
||||||
|
Nokogiri::HTML::Document.parse(@response.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,3 @@
|
||||||
require 'action_view/vendor/html-scanner'
|
require 'active_support/deprecation'
|
||||||
|
|
||||||
module ActionDispatch
|
ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.")
|
||||||
module Assertions
|
|
||||||
module DomAssertions
|
|
||||||
# \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
|
|
||||||
#
|
|
||||||
# # assert that the referenced method generates the appropriate HTML string
|
|
||||||
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
|
|
||||||
def assert_dom_equal(expected, actual, message = nil)
|
|
||||||
expected_dom = HTML::Document.new(expected).root
|
|
||||||
actual_dom = HTML::Document.new(actual).root
|
|
||||||
assert_equal expected_dom, actual_dom, message
|
|
||||||
end
|
|
||||||
|
|
||||||
# The negated form of +assert_dom_equivalent+.
|
|
||||||
#
|
|
||||||
# # assert that the referenced method does not generate the specified HTML string
|
|
||||||
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
|
|
||||||
def assert_dom_not_equal(expected, actual, message = nil)
|
|
||||||
expected_dom = HTML::Document.new(expected).root
|
|
||||||
actual_dom = HTML::Document.new(actual).root
|
|
||||||
assert_not_equal expected_dom, actual_dom, message
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,430 +1,3 @@
|
||||||
require 'action_view/vendor/html-scanner'
|
require 'active_support/deprecation'
|
||||||
require 'active_support/core_ext/object/inclusion'
|
|
||||||
|
|
||||||
#--
|
ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been has been extracted to the rails-dom-testing gem.")
|
||||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
|
||||||
# Under MIT and/or CC By license.
|
|
||||||
#++
|
|
||||||
|
|
||||||
module ActionDispatch
|
|
||||||
module Assertions
|
|
||||||
NO_STRIP = %w{pre script style textarea}
|
|
||||||
|
|
||||||
# Adds the +assert_select+ method for use in Rails functional
|
|
||||||
# test cases, which can be used 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_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 to learn how to use selectors.
|
|
||||||
module SelectorAssertions
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# # Selects all div tags
|
|
||||||
# divs = css_select("div")
|
|
||||||
#
|
|
||||||
# # Selects all paragraph tags and does something interesting
|
|
||||||
# pars = css_select("p")
|
|
||||||
# pars.each do |par|
|
|
||||||
# # Do something fun with paragraphs here...
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# # Selects all list items in unordered lists
|
|
||||||
# items = css_select("ul>li")
|
|
||||||
#
|
|
||||||
# # Selects all form tags and then all inputs inside the form
|
|
||||||
# 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 argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
|
|
||||||
elsif defined?(@selected) && @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
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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+ selects from the
|
|
||||||
# response HTML unless +assert_select+ is called from within an +assert_select+ block.
|
|
||||||
#
|
|
||||||
# When called with a block +assert_select+ passes an array of selected elements
|
|
||||||
# to the block. Calling +assert_select+ from the block, with no element specified,
|
|
||||||
# runs the assertion on the complete set of elements selected by the enclosing assertion.
|
|
||||||
# Alternatively the array may be iterated through so that +assert_select+ can be called
|
|
||||||
# separately for each element.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# ==== Example
|
|
||||||
# If the response contains two ordered lists, each with four list elements then:
|
|
||||||
# assert_select "ol" do |elements|
|
|
||||||
# elements.each do |element|
|
|
||||||
# assert_select element, "li", 4
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# will pass, as will:
|
|
||||||
# assert_select "ol" do
|
|
||||||
# assert_select "li", 8
|
|
||||||
# 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>true</tt> - Assertion is true if at least one element selected.
|
|
||||||
# * <tt>false</tt> - Assertion is true if no element selected.
|
|
||||||
# * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
|
|
||||||
# one element matches the string or regular expression.
|
|
||||||
# * <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.
|
|
||||||
# If no equality test specified, the assertion is true if at least one
|
|
||||||
# element selected.
|
|
||||||
#
|
|
||||||
# To perform more than one equality tests, use a hash with the following keys:
|
|
||||||
# * <tt>:text</tt> - Narrow the selection to elements that have this text
|
|
||||||
# value (string or regexp).
|
|
||||||
# * <tt>:html</tt> - Narrow the selection to elements that have this HTML
|
|
||||||
# content (string or regexp).
|
|
||||||
# * <tt>:count</tt> - Assertion is true if the number of selected elements
|
|
||||||
# is equal to this value.
|
|
||||||
# * <tt>:minimum</tt> - Assertion is true if the number of selected
|
|
||||||
# elements is at least this value.
|
|
||||||
# * <tt>:maximum</tt> - Assertion is true if the number of selected
|
|
||||||
# elements is at most this value.
|
|
||||||
#
|
|
||||||
# If the method is called with a block, once all equality tests are
|
|
||||||
# evaluated the block is called with an array of all matched elements.
|
|
||||||
#
|
|
||||||
# # 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
|
|
||||||
@selected ||= nil
|
|
||||||
|
|
||||||
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 argument 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
|
|
||||||
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
|
|
||||||
|
|
||||||
# By default we're looking for at least one match.
|
|
||||||
if equals[:count]
|
|
||||||
equals[:minimum] = equals[:maximum] = equals[:count]
|
|
||||||
else
|
|
||||||
equals[:minimum] = 1 unless equals[:minimum]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Last argument is the message we use if the assertion fails.
|
|
||||||
message = args.shift
|
|
||||||
#- message = "No match made with selector #{selector.inspect}" unless message
|
|
||||||
if args.shift
|
|
||||||
raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
|
|
||||||
end
|
|
||||||
|
|
||||||
matches = selector.select(root)
|
|
||||||
# If text/html, narrow down to those elements that match it.
|
|
||||||
content_mismatch = nil
|
|
||||||
if match_with = equals[:text]
|
|
||||||
matches.delete_if do |match|
|
|
||||||
text = ""
|
|
||||||
stack = match.children.reverse
|
|
||||||
while node = stack.pop
|
|
||||||
if node.tag?
|
|
||||||
stack.concat node.children.reverse
|
|
||||||
else
|
|
||||||
content = node.content
|
|
||||||
text << content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
text.strip! unless NO_STRIP.include?(match.name)
|
|
||||||
text.sub!(/\A\n/, '') if match.name == "textarea"
|
|
||||||
unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
|
|
||||||
content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text)
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elsif match_with = equals[:html]
|
|
||||||
matches.delete_if do |match|
|
|
||||||
html = match.children.map(&:to_s).join
|
|
||||||
html.strip! unless NO_STRIP.include?(match.name)
|
|
||||||
unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
|
|
||||||
content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html)
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# Expecting foo found bar element only if found zero, not if
|
|
||||||
# found one but expecting two.
|
|
||||||
message ||= content_mismatch if matches.empty?
|
|
||||||
# Test minimum/maximum occurrence.
|
|
||||||
min, max, count = equals[:minimum], equals[:maximum], equals[:count]
|
|
||||||
|
|
||||||
# FIXME: minitest provides messaging when we use assert_operator,
|
|
||||||
# so is this custom message really needed?
|
|
||||||
message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size})
|
|
||||||
if count
|
|
||||||
assert_equal count, matches.size, message
|
|
||||||
else
|
|
||||||
assert_operator matches.size, :>=, min, message if min
|
|
||||||
assert_operator matches.size, :<=, max, message if max
|
|
||||||
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
|
|
||||||
|
|
||||||
def count_description(min, max, count) #:nodoc:
|
|
||||||
pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
|
|
||||||
|
|
||||||
if min && max && (max != min)
|
|
||||||
"between #{min} and #{max} elements"
|
|
||||||
elsif min && max && max == min && count
|
|
||||||
"exactly #{count} #{pluralize['element', min]}"
|
|
||||||
elsif min && !(min == 1 && max == 1)
|
|
||||||
"at least #{min} #{pluralize['element', min]}"
|
|
||||||
elsif max
|
|
||||||
"at most #{max} #{pluralize['element', max]}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
|
|
||||||
# assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
|
|
||||||
# # Select each entry item and then the title item
|
|
||||||
# assert_select "entry>title" do
|
|
||||||
# # Run assertions on the encoded title elements
|
|
||||||
# assert_select_encoded do
|
|
||||||
# assert_select "b"
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# # Selects all paragraph tags from within the description of an RSS feed
|
|
||||||
# assert_select "rss[version=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) { Rack::Utils.escapeHTML($1) }
|
|
||||||
end
|
|
||||||
|
|
||||||
selected = elements.map do |elem|
|
|
||||||
text = elem.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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
#
|
|
||||||
# assert_select_email do
|
|
||||||
# assert_select "h1", "Email alert"
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# assert_select_email do
|
|
||||||
# items = assert_select "ol>li"
|
|
||||||
# items.each do
|
|
||||||
# # Work with items here...
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
def assert_select_email(&block)
|
|
||||||
deliveries = ActionMailer::Base.deliveries
|
|
||||||
assert !deliveries.empty?, "No e-mail in delivery list"
|
|
||||||
|
|
||||||
deliveries.each do |delivery|
|
|
||||||
(delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
|
|
||||||
if part["Content-Type"].to_s =~ /^text\/html\W/
|
|
||||||
root = HTML::Document.new(part.body.to_s).root
|
|
||||||
assert_select root, ":root", &block
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
# +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
|
|
||||||
def response_from_page
|
|
||||||
html_document.root
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,135 +0,0 @@
|
||||||
require 'action_view/vendor/html-scanner'
|
|
||||||
|
|
||||||
module ActionDispatch
|
|
||||||
module Assertions
|
|
||||||
# Pair of assertions to testing elements in the HTML output of the response.
|
|
||||||
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+.
|
|
||||||
#
|
|
||||||
# # 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/ }
|
|
||||||
#
|
|
||||||
# <b>Please note</b>: +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)
|
|
||||||
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
|
|
||||||
|
|
||||||
# Identical to +assert_tag+, but asserts that a matching tag does _not_
|
|
||||||
# exist. (See +assert_tag+ for a full discussion of the syntax.)
|
|
||||||
#
|
|
||||||
# # Assert that there is not a "div" containing a "p"
|
|
||||||
# assert_no_tag tag: "div", descendant: { tag: "p" }
|
|
||||||
#
|
|
||||||
# # Assert that an unordered list is empty
|
|
||||||
# assert_no_tag tag: "ul", descendant: { tag: "li" }
|
|
||||||
#
|
|
||||||
# # Assert that there is not a "p" tag with between 1 to 3 "img" tags
|
|
||||||
# # as immediate children
|
|
||||||
# assert_no_tag tag: "p",
|
|
||||||
# children: { count: 1..3, only: { tag: "img" } }
|
|
||||||
def assert_no_tag(*opts)
|
|
||||||
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
|
|
||||||
|
|
||||||
def find_tag(conditions)
|
|
||||||
html_document.find(conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_all_tag(conditions)
|
|
||||||
html_document.find_all(conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
def html_document
|
|
||||||
xml = @response.content_type =~ /xml$/
|
|
||||||
@html_document ||= HTML::Document.new(@response.body, false, xml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -495,5 +495,9 @@ module ActionDispatch
|
||||||
reset! unless integration_session
|
reset! unless integration_session
|
||||||
integration_session.url_options
|
integration_session.url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def document_root_element
|
||||||
|
html_document.root
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
require 'abstract_unit'
|
require 'abstract_unit'
|
||||||
require 'action_view/vendor/html-scanner'
|
|
||||||
require 'controller/fake_controllers'
|
require 'controller/fake_controllers'
|
||||||
|
|
||||||
class ActionPackAssertionsController < ActionController::Base
|
class ActionPackAssertionsController < ActionController::Base
|
||||||
|
@ -147,11 +146,6 @@ end
|
||||||
|
|
||||||
class ActionPackAssertionsControllerTest < ActionController::TestCase
|
class ActionPackAssertionsControllerTest < ActionController::TestCase
|
||||||
|
|
||||||
def test_assert_tag_and_url_for
|
|
||||||
get :render_url
|
|
||||||
assert_tag :content => "/action_pack_assertions/flash_me"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_file_absolute_path
|
def test_render_file_absolute_path
|
||||||
get :render_file_absolute_path
|
get :render_file_absolute_path
|
||||||
assert_match(/\A= Action Pack/, @response.body)
|
assert_match(/\A= Action Pack/, @response.body)
|
||||||
|
|
|
@ -1,356 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
#--
|
|
||||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
|
||||||
# Under MIT and/or CC By license.
|
|
||||||
#++
|
|
||||||
|
|
||||||
require 'abstract_unit'
|
|
||||||
require 'controller/fake_controllers'
|
|
||||||
|
|
||||||
require 'action_mailer'
|
|
||||||
require 'action_view'
|
|
||||||
|
|
||||||
ActionMailer::Base.send(:include, ActionView::Layouts)
|
|
||||||
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
|
|
||||||
|
|
||||||
class AssertSelectTest < ActionController::TestCase
|
|
||||||
Assertion = ActiveSupport::TestCase::Assertion
|
|
||||||
|
|
||||||
class AssertSelectMailer < ActionMailer::Base
|
|
||||||
def test(html)
|
|
||||||
mail :body => html, :content_type => "text/html",
|
|
||||||
:subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class AssertMultipartSelectMailer < ActionMailer::Base
|
|
||||||
def test(options)
|
|
||||||
mail :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" do |format|
|
|
||||||
format.text { render :text => options[:text] }
|
|
||||||
format.html { render :text => options[:html] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 xml()
|
|
||||||
render :text=>@content, :layout=>false, :content_type=>Mime::XML
|
|
||||||
@content = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
tests AssertSelectController
|
|
||||||
|
|
||||||
def setup
|
|
||||||
super
|
|
||||||
@old_delivery_method = ActionMailer::Base.delivery_method
|
|
||||||
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
|
|
||||||
ActionMailer::Base.delivery_method = :test
|
|
||||||
ActionMailer::Base.perform_deliveries = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def teardown
|
|
||||||
super
|
|
||||||
ActionMailer::Base.delivery_method = @old_delivery_method
|
|
||||||
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
|
|
||||||
ActionMailer::Base.deliveries.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_failure(message, &block)
|
|
||||||
e = assert_raise(Assertion, &block)
|
|
||||||
assert_match(message, e.message) if Regexp === message
|
|
||||||
assert_equal(message, e.message) if String === message
|
|
||||||
end
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test assert select.
|
|
||||||
#
|
|
||||||
|
|
||||||
def test_assert_select
|
|
||||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
|
||||||
assert_select "div", 2
|
|
||||||
assert_failure(/\AExpected at least 1 element matching \"p\", found 0\.$/) { assert_select "p" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_equality_integer
|
|
||||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
|
||||||
assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) { assert_select "div", 3 }
|
|
||||||
assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 0 }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_equality_true_false
|
|
||||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
|
||||||
assert_nothing_raised { assert_select "div" }
|
|
||||||
assert_raise(Assertion) { assert_select "p" }
|
|
||||||
assert_nothing_raised { assert_select "div", true }
|
|
||||||
assert_raise(Assertion) { assert_select "p", true }
|
|
||||||
assert_raise(Assertion) { assert_select "div", false }
|
|
||||||
assert_nothing_raised { assert_select "p", false }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_equality_false_message
|
|
||||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
|
||||||
assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 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_raise(Assertion) { assert_select "div", "bar" }
|
|
||||||
assert_failure(/\A<bar> expected but was\n<foo>\.$/) { assert_select "div", "bar" }
|
|
||||||
assert_nothing_raised { assert_select "div", :text=>"foo" }
|
|
||||||
assert_raise(Assertion) { assert_select "div", :text=>"bar" }
|
|
||||||
assert_nothing_raised { assert_select "div", /(foo|bar)/ }
|
|
||||||
assert_raise(Assertion) { assert_select "div", /foobar/ }
|
|
||||||
assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
|
|
||||||
assert_raise(Assertion) { assert_select "div", :text=>/foobar/ }
|
|
||||||
assert_raise(Assertion) { 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_raise(Assertion) { assert_select "p", html }
|
|
||||||
assert_nothing_raised { assert_select "p", :html=>html }
|
|
||||||
assert_raise(Assertion) { assert_select "p", :html=>text }
|
|
||||||
assert_failure(/\A<#{text}> expected but was\n<#{html}>\.$/) { 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_raise(Assertion) { assert_select "pre", html }
|
|
||||||
assert_nothing_raised { assert_select "pre", :html=>html }
|
|
||||||
assert_raise(Assertion) { assert_select "pre", :html=>text }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_strip_textarea
|
|
||||||
render_html %Q{<textarea>\n\nfoo\n</textarea>}
|
|
||||||
assert_select "textarea", "\nfoo\n"
|
|
||||||
render_html %Q{<textarea>\nfoo</textarea>}
|
|
||||||
assert_select "textarea", "foo"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_counts
|
|
||||||
render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
|
|
||||||
assert_nothing_raised { assert_select "div", 2 }
|
|
||||||
assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", 3
|
|
||||||
end
|
|
||||||
assert_nothing_raised { assert_select "div", 1..2 }
|
|
||||||
assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", 3..4
|
|
||||||
end
|
|
||||||
assert_nothing_raised { assert_select "div", :count=>2 }
|
|
||||||
assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", :count=>3
|
|
||||||
end
|
|
||||||
assert_nothing_raised { assert_select "div", :minimum=>1 }
|
|
||||||
assert_nothing_raised { assert_select "div", :minimum=>2 }
|
|
||||||
assert_failure(/\AExpected at least 3 elements matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", :minimum=>3
|
|
||||||
end
|
|
||||||
assert_nothing_raised { assert_select "div", :maximum=>2 }
|
|
||||||
assert_nothing_raised { assert_select "div", :maximum=>3 }
|
|
||||||
assert_failure(/\AExpected at most 1 element matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", :maximum=>1
|
|
||||||
end
|
|
||||||
assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
|
|
||||||
assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do
|
|
||||||
assert_select "div", :minimum=>3, :maximum=>4
|
|
||||||
end
|
|
||||||
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
|
|
||||||
|
|
||||||
assert_failure(/\AExpected at least 1 element matching \"#4\", found 0\.$/) do
|
|
||||||
assert_select "div" do
|
|
||||||
assert_select "#4"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_select_text_match
|
|
||||||
render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>}
|
|
||||||
assert_select "div" do
|
|
||||||
assert_nothing_raised { assert_select "div", "foo" }
|
|
||||||
assert_nothing_raised { assert_select "div", "bar" }
|
|
||||||
assert_nothing_raised { assert_select "div", /\w*/ }
|
|
||||||
assert_nothing_raised { assert_select "div", :text => /\w*/, :count=>2 }
|
|
||||||
assert_raise(Assertion) { assert_select "div", :text=>"foo", :count=>2 }
|
|
||||||
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
|
|
||||||
assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
|
|
||||||
assert_nothing_raised { assert_select "div", :html=>/\w*/ }
|
|
||||||
assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 }
|
|
||||||
assert_raise(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_elect_with_xml_namespace_attributes
|
|
||||||
render_html %Q{<link xlink:href="http://nowhere.com"></link>}
|
|
||||||
assert_nothing_raised { assert_select "link[xlink:href=http://nowhere.com]" }
|
|
||||||
end
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test css_select.
|
|
||||||
#
|
|
||||||
|
|
||||||
def test_css_select
|
|
||||||
render_html %Q{<div id="1"></div><div id="2"></div>}
|
|
||||||
assert_equal 2, css_select("div").size
|
|
||||||
assert_equal 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_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_raise(Assertion) { assert_select_email {} }
|
|
||||||
AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
|
|
||||||
assert_select_email do
|
|
||||||
assert_select "div:root" do
|
|
||||||
assert_select "p:first-child", "foo"
|
|
||||||
assert_select "p:last-child", "bar"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_select_email_multipart
|
|
||||||
AssertMultipartSelectMailer.test(:html => "<div><p>foo</p><p>bar</p></div>", :text => 'foo bar').deliver
|
|
||||||
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_xml(xml)
|
|
||||||
@controller.response_with = xml
|
|
||||||
get :xml
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +1,5 @@
|
||||||
require 'abstract_unit'
|
require 'abstract_unit'
|
||||||
require 'controller/fake_controllers'
|
require 'controller/fake_controllers'
|
||||||
require 'action_view/vendor/html-scanner'
|
|
||||||
require 'rails/engine'
|
require 'rails/engine'
|
||||||
|
|
||||||
class SessionTest < ActiveSupport::TestCase
|
class SessionTest < ActiveSupport::TestCase
|
||||||
|
@ -293,7 +292,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal({}, cookies.to_hash)
|
assert_equal({}, cookies.to_hash)
|
||||||
assert_equal "OK", body
|
assert_equal "OK", body
|
||||||
assert_equal "OK", response.body
|
assert_equal "OK", response.body
|
||||||
assert_kind_of HTML::Document, html_document
|
assert_kind_of Nokogiri::HTML::Document, html_document
|
||||||
assert_equal 1, request_count
|
assert_equal 1, request_count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -309,7 +308,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
|
||||||
assert_equal({}, cookies.to_hash)
|
assert_equal({}, cookies.to_hash)
|
||||||
assert_equal "Created", body
|
assert_equal "Created", body
|
||||||
assert_equal "Created", response.body
|
assert_equal "Created", response.body
|
||||||
assert_kind_of HTML::Document, html_document
|
assert_kind_of Nokogiri::HTML::Document, html_document
|
||||||
assert_equal 1, request_count
|
assert_equal 1, request_count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -369,7 +368,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
|
||||||
assert_response :redirect
|
assert_response :redirect
|
||||||
assert_response :found
|
assert_response :found
|
||||||
assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
|
assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
|
||||||
assert_kind_of HTML::Document, html_document
|
assert_kind_of Nokogiri::HTML::Document, html_document
|
||||||
assert_equal 1, request_count
|
assert_equal 1, request_count
|
||||||
|
|
||||||
follow_redirect!
|
follow_redirect!
|
||||||
|
|
|
@ -389,7 +389,8 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
|
||||||
SecureRandom.stubs(:base64).returns(@token + '<=?')
|
SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||||
get :meta
|
get :meta
|
||||||
assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
|
assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
|
||||||
assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae<=?'
|
assert_select 'meta[name=?]', 'csrf-token'
|
||||||
|
assert_match(/cf50faa3fe97702ca1ae<=\?/, @response.body)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,629 +0,0 @@
|
||||||
#--
|
|
||||||
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
|
|
||||||
# Under MIT and/or CC By license.
|
|
||||||
#++
|
|
||||||
|
|
||||||
require 'abstract_unit'
|
|
||||||
require 'controller/fake_controllers'
|
|
||||||
require 'action_view/vendor/html-scanner'
|
|
||||||
|
|
||||||
class SelectorTest < ActiveSupport::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 element with attribute value.
|
|
||||||
select("*[title=foo]")
|
|
||||||
assert_equal 1, @matches.size
|
|
||||||
assert_equal "3", @matches[0].attributes["id"]
|
|
||||||
# Match element 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 challenge.
|
|
||||||
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_raise(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_only_child_and_only_type_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_raise(ArgumentError) { select(":not(") }
|
|
||||||
assert_raise(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
|
|
|
@ -353,168 +353,6 @@ XML
|
||||||
assert_equal "bar", assigns[:bar]
|
assert_equal "bar", assigns[:bar]
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assert_tag_tag
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a 'form' tag
|
|
||||||
assert_tag :tag => 'form'
|
|
||||||
# there is not an 'hr' tag
|
|
||||||
assert_no_tag :tag => 'hr'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_attributes
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with an 'id' of 'bar'
|
|
||||||
assert_tag :attributes => { :id => "bar" }
|
|
||||||
# there is no tag with a 'name' of 'baz'
|
|
||||||
assert_no_tag :attributes => { :name => "baz" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_parent
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with a parent 'form' tag
|
|
||||||
assert_tag :parent => { :tag => "form" }
|
|
||||||
# there is no tag with a parent of 'input'
|
|
||||||
assert_no_tag :parent => { :tag => "input" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_child
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with a child 'input' tag
|
|
||||||
assert_tag :child => { :tag => "input" }
|
|
||||||
# there is no tag with a child 'strong' tag
|
|
||||||
assert_no_tag :child => { :tag => "strong" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_ancestor
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a 'li' tag with an ancestor having an id of 'foo'
|
|
||||||
assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li"
|
|
||||||
# there is no tag of any kind with an ancestor having an href matching 'foo'
|
|
||||||
assert_no_tag :ancestor => { :attributes => { :href => /foo/ } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_descendant
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with a descendant 'li' tag
|
|
||||||
assert_tag :descendant => { :tag => "li" }
|
|
||||||
# there is no tag with a descendant 'html' tag
|
|
||||||
assert_no_tag :descendant => { :tag => "html" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_sibling
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with a sibling of class 'item'
|
|
||||||
assert_tag :sibling => { :attributes => { :class => "item" } }
|
|
||||||
# there is no tag with a sibling 'ul' tag
|
|
||||||
assert_no_tag :sibling => { :tag => "ul" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_after
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag following a sibling 'div' tag
|
|
||||||
assert_tag :after => { :tag => "div" }
|
|
||||||
# there is no tag following a sibling tag with id 'bar'
|
|
||||||
assert_no_tag :after => { :attributes => { :id => "bar" } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_before
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag preceding a tag with id 'bar'
|
|
||||||
assert_tag :before => { :attributes => { :id => "bar" } }
|
|
||||||
# there is no tag preceding a 'form' tag
|
|
||||||
assert_no_tag :before => { :tag => "form" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_children_count
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with 2 children
|
|
||||||
assert_tag :children => { :count => 2 }
|
|
||||||
# in particular, there is a <ul> tag with two children (a nameless pair of <li>s)
|
|
||||||
assert_tag :tag => 'ul', :children => { :count => 2 }
|
|
||||||
# there is no tag with 4 children
|
|
||||||
assert_no_tag :children => { :count => 4 }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_children_less_than
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag with less than 5 children
|
|
||||||
assert_tag :children => { :less_than => 5 }
|
|
||||||
# there is no 'ul' tag with less than 2 children
|
|
||||||
assert_no_tag :children => { :less_than => 2 }, :tag => "ul"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_children_greater_than
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a 'body' tag with more than 1 children
|
|
||||||
assert_tag :children => { :greater_than => 1 }, :tag => "body"
|
|
||||||
# there is no tag with more than 10 children
|
|
||||||
assert_no_tag :children => { :greater_than => 10 }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_children_only
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a tag containing only one child with an id of 'foo'
|
|
||||||
assert_tag :children => { :count => 1,
|
|
||||||
:only => { :attributes => { :id => "foo" } } }
|
|
||||||
# there is no tag containing only one 'li' child
|
|
||||||
assert_no_tag :children => { :count => 1, :only => { :tag => "li" } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_content
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# the output contains the string "Name"
|
|
||||||
assert_tag :content => /Name/
|
|
||||||
# the output does not contain the string "test"
|
|
||||||
assert_no_tag :content => /test/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_multiple
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a 'div', id='bar', with an immediate child whose 'action'
|
|
||||||
# attribute matches the regexp /somewhere/.
|
|
||||||
assert_tag :tag => "div", :attributes => { :id => "bar" },
|
|
||||||
:child => { :attributes => { :action => /somewhere/ } }
|
|
||||||
|
|
||||||
# there is no 'div', id='foo', with a 'ul' child with more than
|
|
||||||
# 2 "li" children.
|
|
||||||
assert_no_tag :tag => "div", :attributes => { :id => "foo" },
|
|
||||||
:child => {
|
|
||||||
:tag => "ul",
|
|
||||||
:children => { :greater_than => 2,
|
|
||||||
:only => { :tag => "li" } } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_children_without_content
|
|
||||||
process :test_html_output
|
|
||||||
|
|
||||||
# there is a form tag with an 'input' child which is a self closing tag
|
|
||||||
assert_tag :tag => "form",
|
|
||||||
:children => { :count => 1,
|
|
||||||
:only => { :tag => "input" } }
|
|
||||||
|
|
||||||
# the body tag has an 'a' child which in turn has an 'img' child
|
|
||||||
assert_tag :tag => "body",
|
|
||||||
:children => { :count => 1,
|
|
||||||
:only => { :tag => "a",
|
|
||||||
:children => { :count => 1,
|
|
||||||
:only => { :tag => "img" } } } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_not_impose_childless_html_tags_in_xml
|
def test_should_not_impose_childless_html_tags_in_xml
|
||||||
process :test_xml_output
|
process :test_xml_output
|
||||||
|
|
||||||
|
@ -529,23 +367,6 @@ XML
|
||||||
assert err.empty?
|
assert err.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_assert_tag_attribute_matching
|
|
||||||
@response.body = '<input type="text" name="my_name">'
|
|
||||||
assert_tag :tag => 'input',
|
|
||||||
:attributes => { :name => /my/, :type => 'text' }
|
|
||||||
assert_no_tag :tag => 'input',
|
|
||||||
:attributes => { :name => 'my', :type => 'text' }
|
|
||||||
assert_no_tag :tag => 'input',
|
|
||||||
:attributes => { :name => /^my$/, :type => 'text' }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_tag_content_matching
|
|
||||||
@response.body = "<p>hello world</p>"
|
|
||||||
assert_tag :tag => "p", :content => "hello world"
|
|
||||||
assert_tag :tag => "p", :content => /hello/
|
|
||||||
assert_no_tag :tag => "p", :content => "hello"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assert_generates
|
def test_assert_generates
|
||||||
assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5'
|
assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5'
|
||||||
assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"}
|
assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"}
|
||||||
|
|
|
@ -23,6 +23,7 @@ Gem::Specification.new do |s|
|
||||||
|
|
||||||
s.add_dependency 'builder', '~> 3.1'
|
s.add_dependency 'builder', '~> 3.1'
|
||||||
s.add_dependency 'erubis', '~> 2.7.0'
|
s.add_dependency 'erubis', '~> 2.7.0'
|
||||||
|
s.add_dependency 'rails-deprecated_sanitizer'
|
||||||
|
|
||||||
s.add_development_dependency 'actionpack', version
|
s.add_development_dependency 'actionpack', version
|
||||||
s.add_development_dependency 'activemodel', version
|
s.add_development_dependency 'activemodel', version
|
||||||
|
|
|
@ -86,7 +86,6 @@ module ActionView
|
||||||
super
|
super
|
||||||
ActionView::Helpers.eager_load!
|
ActionView::Helpers.eager_load!
|
||||||
ActionView::Template.eager_load!
|
ActionView::Template.eager_load!
|
||||||
HTML.eager_load!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
require 'active_support/core_ext/object/try'
|
require 'active_support/core_ext/object/try'
|
||||||
require 'action_view/vendor/html-scanner'
|
require 'active_support/deprecation'
|
||||||
|
require 'rails-deprecated_sanitizer'
|
||||||
|
|
||||||
module ActionView
|
module ActionView
|
||||||
# = Action View Sanitize Helpers
|
# = Action View Sanitize Helpers
|
||||||
|
@ -27,7 +28,29 @@ module ActionView
|
||||||
#
|
#
|
||||||
# <%= sanitize @article.body %>
|
# <%= sanitize @article.body %>
|
||||||
#
|
#
|
||||||
# Custom Use (only the mentioned tags and attributes are allowed, nothing else)
|
# Custom Use - Custom Scrubber
|
||||||
|
# (supply a Loofah::Scrubber that does the sanitization)
|
||||||
|
#
|
||||||
|
# scrubber can either wrap a block:
|
||||||
|
# scrubber = Loofah::Scrubber.new do |node|
|
||||||
|
# node.text = "dawn of cats"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# or be a subclass of Loofah::Scrubber which responds to scrub:
|
||||||
|
# class KittyApocalypse < Loofah::Scrubber
|
||||||
|
# def scrub(node)
|
||||||
|
# node.text = "dawn of cats"
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# scrubber = KittyApocalypse.new
|
||||||
|
#
|
||||||
|
# <%= sanitize @article.body, scrubber: scrubber %>
|
||||||
|
#
|
||||||
|
# A custom scrubber takes precedence over custom tags and attributes
|
||||||
|
# Learn more about scrubbers here: https://github.com/flavorjones/loofah
|
||||||
|
#
|
||||||
|
# Custom Use - tags and attributes
|
||||||
|
# (only the mentioned tags and attributes are allowed, nothing else)
|
||||||
#
|
#
|
||||||
# <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
|
# <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
|
||||||
#
|
#
|
||||||
|
@ -65,9 +88,9 @@ module ActionView
|
||||||
self.class.white_list_sanitizer.sanitize_css(style)
|
self.class.white_list_sanitizer.sanitize_css(style)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Strips all HTML tags from the +html+, including comments. This uses the
|
# Strips all HTML tags from the +html+, including comments. This uses
|
||||||
# html-scanner tokenizer and so its HTML parsing ability is limited by
|
# Nokogiri for tokenization (via Loofah) and so its HTML parsing ability
|
||||||
# that of html-scanner.
|
# is limited by that of Nokogiri.
|
||||||
#
|
#
|
||||||
# strip_tags("Strip <i>these</i> tags!")
|
# strip_tags("Strip <i>these</i> tags!")
|
||||||
# # => Strip these tags!
|
# # => Strip these tags!
|
||||||
|
@ -98,47 +121,42 @@ module ActionView
|
||||||
module ClassMethods #:nodoc:
|
module ClassMethods #:nodoc:
|
||||||
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
|
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
|
||||||
|
|
||||||
def sanitized_protocol_separator
|
[:protocol_separator,
|
||||||
white_list_sanitizer.protocol_separator
|
:uri_attributes,
|
||||||
|
:bad_tags,
|
||||||
|
:allowed_css_properties,
|
||||||
|
:allowed_css_keywords,
|
||||||
|
:shorthand_css_properties,
|
||||||
|
:allowed_protocols].each do |meth|
|
||||||
|
meth_name = "sanitized_#{meth}"
|
||||||
|
imp = lambda do |name|
|
||||||
|
ActiveSupport::Deprecation.warn("#{name} is deprecated and has no effect.")
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_uri_attributes
|
define_method(meth_name) { imp.(meth_name) }
|
||||||
white_list_sanitizer.uri_attributes
|
define_method("#{meth_name}=") { |value| imp.("#{meth_name}=") }
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_bad_tags
|
# Vendors the full, link and white list sanitizers.
|
||||||
white_list_sanitizer.bad_tags
|
# This uses html-scanner for the HTML sanitization.
|
||||||
|
# In the next Rails version this will use Rails::Html::Sanitizer instead.
|
||||||
|
# To get this new behavior now, in your Gemfile, add:
|
||||||
|
#
|
||||||
|
# gem 'rails-html-sanitizer'
|
||||||
|
#
|
||||||
|
def sanitizer_vendor
|
||||||
|
Rails::DeprecatedSanitizer
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_allowed_tags
|
def sanitized_allowed_tags
|
||||||
white_list_sanitizer.allowed_tags
|
sanitizer_vendor.white_list_sanitizer.allowed_tags
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_allowed_attributes
|
def sanitized_allowed_attributes
|
||||||
white_list_sanitizer.allowed_attributes
|
sanitizer_vendor.white_list_sanitizer.allowed_attributes
|
||||||
end
|
end
|
||||||
|
|
||||||
def sanitized_allowed_css_properties
|
# Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
|
||||||
white_list_sanitizer.allowed_css_properties
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitized_allowed_css_keywords
|
|
||||||
white_list_sanitizer.allowed_css_keywords
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitized_shorthand_css_properties
|
|
||||||
white_list_sanitizer.shorthand_css_properties
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitized_allowed_protocols
|
|
||||||
white_list_sanitizer.allowed_protocols
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitized_protocol_separator=(value)
|
|
||||||
white_list_sanitizer.protocol_separator = value
|
|
||||||
end
|
|
||||||
|
|
||||||
# Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
|
|
||||||
# any object that responds to +sanitize+.
|
# any object that responds to +sanitize+.
|
||||||
#
|
#
|
||||||
# class Application < Rails::Application
|
# class Application < Rails::Application
|
||||||
|
@ -146,21 +164,21 @@ module ActionView
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def full_sanitizer
|
def full_sanitizer
|
||||||
@full_sanitizer ||= HTML::FullSanitizer.new
|
@full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
|
# Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
|
||||||
# any object that responds to +sanitize+.
|
# Replace with any object that responds to +sanitize+.
|
||||||
#
|
#
|
||||||
# class Application < Rails::Application
|
# class Application < Rails::Application
|
||||||
# config.action_view.link_sanitizer = MySpecialSanitizer.new
|
# config.action_view.link_sanitizer = MySpecialSanitizer.new
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def link_sanitizer
|
def link_sanitizer
|
||||||
@link_sanitizer ||= HTML::LinkSanitizer.new
|
@link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
|
# Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
|
||||||
# Replace with any object that responds to +sanitize+.
|
# Replace with any object that responds to +sanitize+.
|
||||||
#
|
#
|
||||||
# class Application < Rails::Application
|
# class Application < Rails::Application
|
||||||
|
@ -168,87 +186,27 @@ module ActionView
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def white_list_sanitizer
|
def white_list_sanitizer
|
||||||
@white_list_sanitizer ||= HTML::WhiteListSanitizer.new
|
@white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
|
# Replaces the allowed tags for the +sanitize+ helper.
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_uri_attributes=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of 'bad' tags for the +sanitize+ helper.
|
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_bad_tags = 'embed', 'object'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_bad_tags=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.bad_tags.merge(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of allowed tags for the +sanitize+ helper.
|
|
||||||
#
|
#
|
||||||
# class Application < Rails::Application
|
# class Application < Rails::Application
|
||||||
# config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
|
# config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def sanitized_allowed_tags=(attributes)
|
def sanitized_allowed_tags=(tags)
|
||||||
HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
|
sanitizer_vendor.white_list_sanitizer.allowed_tags = tags
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
|
# Replaces the allowed HTML attributes for the +sanitize+ helper.
|
||||||
#
|
#
|
||||||
# class Application < Rails::Application
|
# class Application < Rails::Application
|
||||||
# config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
|
# config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def sanitized_allowed_attributes=(attributes)
|
def sanitized_allowed_attributes=(attributes)
|
||||||
HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
|
sanitizer_vendor.white_list_sanitizer.allowed_attributes = attributes
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
|
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_allowed_css_properties = 'expression'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_allowed_css_properties=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
|
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_allowed_css_keywords = 'expression'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_allowed_css_keywords=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
|
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_shorthand_css_properties = 'expression'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_shorthand_css_properties=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Adds to the Set of allowed protocols for the +sanitize+ helper.
|
|
||||||
#
|
|
||||||
# class Application < Rails::Application
|
|
||||||
# config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
def sanitized_allowed_protocols=(attributes)
|
|
||||||
HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,8 @@ require 'action_controller'
|
||||||
require 'action_controller/test_case'
|
require 'action_controller/test_case'
|
||||||
require 'action_view'
|
require 'action_view'
|
||||||
|
|
||||||
|
require 'rails-dom-testing'
|
||||||
|
|
||||||
module ActionView
|
module ActionView
|
||||||
# = Action View Test Case
|
# = Action View Test Case
|
||||||
class TestCase < ActiveSupport::TestCase
|
class TestCase < ActiveSupport::TestCase
|
||||||
|
@ -34,6 +36,7 @@ module ActionView
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include ActionDispatch::Assertions, ActionDispatch::TestProcess
|
include ActionDispatch::Assertions, ActionDispatch::TestProcess
|
||||||
|
include Rails::Dom::Testing::Assertions
|
||||||
include ActionController::TemplateAssertions
|
include ActionController::TemplateAssertions
|
||||||
include ActionView::Context
|
include ActionView::Context
|
||||||
|
|
||||||
|
@ -99,7 +102,9 @@ module ActionView
|
||||||
def setup_with_controller
|
def setup_with_controller
|
||||||
@controller = ActionView::TestCase::TestController.new
|
@controller = ActionView::TestCase::TestController.new
|
||||||
@request = @controller.request
|
@request = @controller.request
|
||||||
@output_buffer = ActiveSupport::SafeBuffer.new
|
# empty string ensures buffer has UTF-8 encoding as
|
||||||
|
# new without arguments returns ASCII-8BIT encoded buffer like String#new
|
||||||
|
@output_buffer = ActiveSupport::SafeBuffer.new ''
|
||||||
@rendered = ''
|
@rendered = ''
|
||||||
|
|
||||||
make_test_case_available_to_view!
|
make_test_case_available_to_view!
|
||||||
|
@ -151,11 +156,10 @@ module ActionView
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Support the selector assertions
|
|
||||||
#
|
|
||||||
# Need to experiment if this priority is the best one: rendered => output_buffer
|
# Need to experiment if this priority is the best one: rendered => output_buffer
|
||||||
def response_from_page
|
def document_root_element
|
||||||
HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root
|
@html_document ||= Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered)
|
||||||
|
@html_document.root
|
||||||
end
|
end
|
||||||
|
|
||||||
def say_no_to_protect_against_forgery!
|
def say_no_to_protect_against_forgery!
|
||||||
|
@ -236,7 +240,8 @@ module ActionView
|
||||||
:@test_passed,
|
:@test_passed,
|
||||||
:@view,
|
:@view,
|
||||||
:@view_context_class,
|
:@view_context_class,
|
||||||
:@_subscribers
|
:@_subscribers,
|
||||||
|
:@html_document
|
||||||
]
|
]
|
||||||
|
|
||||||
def _user_defined_ivars
|
def _user_defined_ivars
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner"
|
|
||||||
|
|
||||||
module HTML
|
|
||||||
extend ActiveSupport::Autoload
|
|
||||||
|
|
||||||
eager_autoload do
|
|
||||||
autoload :CDATA, 'html/node'
|
|
||||||
autoload :Document, 'html/document'
|
|
||||||
autoload :FullSanitizer, 'html/sanitizer'
|
|
||||||
autoload :LinkSanitizer, 'html/sanitizer'
|
|
||||||
autoload :Node, 'html/node'
|
|
||||||
autoload :Sanitizer, 'html/sanitizer'
|
|
||||||
autoload :Selector, 'html/selector'
|
|
||||||
autoload :Tag, 'html/node'
|
|
||||||
autoload :Text, 'html/node'
|
|
||||||
autoload :Tokenizer, 'html/tokenizer'
|
|
||||||
autoload :Version, 'html/version'
|
|
||||||
autoload :WhiteListSanitizer, 'html/sanitizer'
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,68 +0,0 @@
|
||||||
require 'html/tokenizer'
|
|
||||||
require 'html/node'
|
|
||||||
require 'html/selector'
|
|
||||||
require 'html/sanitizer'
|
|
||||||
|
|
||||||
module HTML #:nodoc:
|
|
||||||
# A top-level HTML document. You give it a body of text, and it will parse that
|
|
||||||
# text into a tree of nodes.
|
|
||||||
class Document #:nodoc:
|
|
||||||
|
|
||||||
# The root of the parsed document.
|
|
||||||
attr_reader :root
|
|
||||||
|
|
||||||
# Create a new Document from the given text.
|
|
||||||
def initialize(text, strict=false, xml=false)
|
|
||||||
tokenizer = Tokenizer.new(text)
|
|
||||||
@root = Node.new(nil)
|
|
||||||
node_stack = [ @root ]
|
|
||||||
while token = tokenizer.next
|
|
||||||
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
|
|
||||||
|
|
||||||
node_stack.last.children << node unless node.tag? && node.closing == :close
|
|
||||||
if node.tag?
|
|
||||||
if node_stack.length > 1 && node.closing == :close
|
|
||||||
if node_stack.last.name == node.name
|
|
||||||
if node_stack.last.children.empty?
|
|
||||||
node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
|
|
||||||
end
|
|
||||||
node_stack.pop
|
|
||||||
else
|
|
||||||
open_start = node_stack.last.position - 20
|
|
||||||
open_start = 0 if open_start < 0
|
|
||||||
close_start = node.position - 20
|
|
||||||
close_start = 0 if close_start < 0
|
|
||||||
msg = <<EOF.strip
|
|
||||||
ignoring attempt to close #{node_stack.last.name} with #{node.name}
|
|
||||||
opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
|
|
||||||
closed at byte #{node.position}, line #{node.line}
|
|
||||||
attributes at open: #{node_stack.last.attributes.inspect}
|
|
||||||
text around open: #{text[open_start,40].inspect}
|
|
||||||
text around close: #{text[close_start,40].inspect}
|
|
||||||
EOF
|
|
||||||
strict ? raise(msg) : warn(msg)
|
|
||||||
end
|
|
||||||
elsif !node.childless?(xml) && node.closing != :close
|
|
||||||
node_stack.push node
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Search the tree for (and return) the first node that matches the given
|
|
||||||
# conditions. The conditions are interpreted differently for different node
|
|
||||||
# types, see HTML::Text#find and HTML::Tag#find.
|
|
||||||
def find(conditions)
|
|
||||||
@root.find(conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Search the tree for (and return) all nodes that match the given
|
|
||||||
# conditions. The conditions are interpreted differently for different node
|
|
||||||
# types, see HTML::Text#find and HTML::Tag#find.
|
|
||||||
def find_all(conditions)
|
|
||||||
@root.find_all(conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1,532 +0,0 @@
|
||||||
require 'strscan'
|
|
||||||
|
|
||||||
module HTML #:nodoc:
|
|
||||||
|
|
||||||
class Conditions < Hash #:nodoc:
|
|
||||||
def initialize(hash)
|
|
||||||
super()
|
|
||||||
hash = { :content => hash } unless Hash === hash
|
|
||||||
hash = keys_to_symbols(hash)
|
|
||||||
hash.each do |k,v|
|
|
||||||
case k
|
|
||||||
when :tag, :content then
|
|
||||||
# keys are valid, and require no further processing
|
|
||||||
when :attributes then
|
|
||||||
hash[k] = keys_to_strings(v)
|
|
||||||
when :parent, :child, :ancestor, :descendant, :sibling, :before,
|
|
||||||
:after
|
|
||||||
hash[k] = Conditions.new(v)
|
|
||||||
when :children
|
|
||||||
hash[k] = v = keys_to_symbols(v)
|
|
||||||
v.each do |key,value|
|
|
||||||
case key
|
|
||||||
when :count, :greater_than, :less_than
|
|
||||||
# keys are valid, and require no further processing
|
|
||||||
when :only
|
|
||||||
v[key] = Conditions.new(value)
|
|
||||||
else
|
|
||||||
raise "illegal key #{key.inspect} => #{value.inspect}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise "illegal key #{k.inspect} => #{v.inspect}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
update hash
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def keys_to_strings(hash)
|
|
||||||
Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
|
|
||||||
end
|
|
||||||
|
|
||||||
def keys_to_symbols(hash)
|
|
||||||
Hash[hash.keys.map do |k|
|
|
||||||
raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
|
|
||||||
[k.to_sym, hash[k]]
|
|
||||||
end]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# The base class of all nodes, textual and otherwise, in an HTML document.
|
|
||||||
class Node #:nodoc:
|
|
||||||
# The array of children of this node. Not all nodes have children.
|
|
||||||
attr_reader :children
|
|
||||||
|
|
||||||
# The parent node of this node. All nodes have a parent, except for the
|
|
||||||
# root node.
|
|
||||||
attr_reader :parent
|
|
||||||
|
|
||||||
# The line number of the input where this node was begun
|
|
||||||
attr_reader :line
|
|
||||||
|
|
||||||
# The byte position in the input where this node was begun
|
|
||||||
attr_reader :position
|
|
||||||
|
|
||||||
# Create a new node as a child of the given parent.
|
|
||||||
def initialize(parent, line=0, pos=0)
|
|
||||||
@parent = parent
|
|
||||||
@children = []
|
|
||||||
@line, @position = line, pos
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns a textual representation of the node.
|
|
||||||
def to_s
|
|
||||||
@children.join()
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns false (subclasses must override this to provide specific matching
|
|
||||||
# behavior.) +conditions+ may be of any type.
|
|
||||||
def match(conditions)
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Search the children of this node for the first node for which #find
|
|
||||||
# returns non +nil+. Returns the result of the #find call that succeeded.
|
|
||||||
def find(conditions)
|
|
||||||
conditions = validate_conditions(conditions)
|
|
||||||
@children.each do |child|
|
|
||||||
node = child.find(conditions)
|
|
||||||
return node if node
|
|
||||||
end
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Search for all nodes that match the given conditions, and return them
|
|
||||||
# as an array.
|
|
||||||
def find_all(conditions)
|
|
||||||
conditions = validate_conditions(conditions)
|
|
||||||
|
|
||||||
matches = []
|
|
||||||
matches << self if match(conditions)
|
|
||||||
@children.each do |child|
|
|
||||||
matches.concat child.find_all(conditions)
|
|
||||||
end
|
|
||||||
matches
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns +false+. Subclasses may override this if they define a kind of
|
|
||||||
# tag.
|
|
||||||
def tag?
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_conditions(conditions)
|
|
||||||
Conditions === conditions ? conditions : Conditions.new(conditions)
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(node)
|
|
||||||
return false unless self.class == node.class && children.size == node.children.size
|
|
||||||
|
|
||||||
equivalent = true
|
|
||||||
|
|
||||||
children.size.times do |i|
|
|
||||||
equivalent &&= children[i] == node.children[i]
|
|
||||||
end
|
|
||||||
|
|
||||||
equivalent
|
|
||||||
end
|
|
||||||
|
|
||||||
class <<self
|
|
||||||
def parse(parent, line, pos, content, strict=true)
|
|
||||||
if content !~ /^<\S/
|
|
||||||
Text.new(parent, line, pos, content)
|
|
||||||
else
|
|
||||||
scanner = StringScanner.new(content)
|
|
||||||
|
|
||||||
unless scanner.skip(/</)
|
|
||||||
if strict
|
|
||||||
raise "expected <"
|
|
||||||
else
|
|
||||||
return Text.new(parent, line, pos, content)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if scanner.skip(/!\[CDATA\[/)
|
|
||||||
unless scanner.skip_until(/\]\]>/)
|
|
||||||
if strict
|
|
||||||
raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
|
|
||||||
else
|
|
||||||
scanner.skip_until(/\Z/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
|
|
||||||
end
|
|
||||||
|
|
||||||
closing = ( scanner.scan(/\//) ? :close : nil )
|
|
||||||
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
|
|
||||||
name.downcase!
|
|
||||||
|
|
||||||
unless closing
|
|
||||||
scanner.skip(/\s*/)
|
|
||||||
attributes = {}
|
|
||||||
while attr = scanner.scan(/[-\w:]+/)
|
|
||||||
value = true
|
|
||||||
if scanner.scan(/\s*=\s*/)
|
|
||||||
if delim = scanner.scan(/['"]/)
|
|
||||||
value = ""
|
|
||||||
while text = scanner.scan(/[^#{delim}\\]+|./)
|
|
||||||
case text
|
|
||||||
when "\\" then
|
|
||||||
value << text
|
|
||||||
break if scanner.eos?
|
|
||||||
value << scanner.getch
|
|
||||||
when delim
|
|
||||||
break
|
|
||||||
else value << text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
value = scanner.scan(/[^\s>\/]+/)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
attributes[attr.downcase] = value
|
|
||||||
scanner.skip(/\s*/)
|
|
||||||
end
|
|
||||||
|
|
||||||
closing = ( scanner.scan(/\//) ? :self : nil )
|
|
||||||
end
|
|
||||||
|
|
||||||
unless scanner.scan(/\s*>/)
|
|
||||||
if strict
|
|
||||||
raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
|
|
||||||
else
|
|
||||||
# throw away all text until we find what we're looking for
|
|
||||||
scanner.skip_until(/>/) or scanner.terminate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Tag.new(parent, line, pos, name, attributes, closing)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A node that represents text, rather than markup.
|
|
||||||
class Text < Node #:nodoc:
|
|
||||||
|
|
||||||
attr_reader :content
|
|
||||||
|
|
||||||
# Creates a new text node as a child of the given parent, with the given
|
|
||||||
# content.
|
|
||||||
def initialize(parent, line, pos, content)
|
|
||||||
super(parent, line, pos)
|
|
||||||
@content = content
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the content of this node.
|
|
||||||
def to_s
|
|
||||||
@content
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns +self+ if this node meets the given conditions. Text nodes support
|
|
||||||
# conditions of the following kinds:
|
|
||||||
#
|
|
||||||
# * if +conditions+ is a string, it must be a substring of the node's
|
|
||||||
# content
|
|
||||||
# * if +conditions+ is a regular expression, it must match the node's
|
|
||||||
# content
|
|
||||||
# * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
|
|
||||||
# is either a string or a regexp, and which is interpreted as described
|
|
||||||
# above.
|
|
||||||
def find(conditions)
|
|
||||||
match(conditions) && self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns non-+nil+ if this node meets the given conditions, or +nil+
|
|
||||||
# otherwise. See the discussion of #find for the valid conditions.
|
|
||||||
def match(conditions)
|
|
||||||
case conditions
|
|
||||||
when String
|
|
||||||
@content == conditions
|
|
||||||
when Regexp
|
|
||||||
@content =~ conditions
|
|
||||||
when Hash
|
|
||||||
conditions = validate_conditions(conditions)
|
|
||||||
|
|
||||||
# Text nodes only have :content, :parent, :ancestor
|
|
||||||
unless (conditions.keys - [:content, :parent, :ancestor]).empty?
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
match(conditions[:content])
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(node)
|
|
||||||
return false unless super
|
|
||||||
content == node.content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A CDATA node is simply a text node with a specialized way of displaying
|
|
||||||
# itself.
|
|
||||||
class CDATA < Text #:nodoc:
|
|
||||||
def to_s
|
|
||||||
"<![CDATA[#{super}]]>"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A Tag is any node that represents markup. It may be an opening tag, a
|
|
||||||
# closing tag, or a self-closing tag. It has a name, and may have a hash of
|
|
||||||
# attributes.
|
|
||||||
class Tag < Node #:nodoc:
|
|
||||||
|
|
||||||
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
|
|
||||||
attr_reader :closing
|
|
||||||
|
|
||||||
# Either +nil+, or a hash of attributes for this node.
|
|
||||||
attr_reader :attributes
|
|
||||||
|
|
||||||
# The name of this tag.
|
|
||||||
attr_reader :name
|
|
||||||
|
|
||||||
# Create a new node as a child of the given parent, using the given content
|
|
||||||
# to describe the node. It will be parsed and the node name, attributes and
|
|
||||||
# closing status extracted.
|
|
||||||
def initialize(parent, line, pos, name, attributes, closing)
|
|
||||||
super(parent, line, pos)
|
|
||||||
@name = name
|
|
||||||
@attributes = attributes
|
|
||||||
@closing = closing
|
|
||||||
end
|
|
||||||
|
|
||||||
# A convenience for obtaining an attribute of the node. Returns +nil+ if
|
|
||||||
# the node has no attributes.
|
|
||||||
def [](attr)
|
|
||||||
@attributes ? @attributes[attr] : nil
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns non-+nil+ if this tag can contain child nodes.
|
|
||||||
def childless?(xml = false)
|
|
||||||
return false if xml && @closing.nil?
|
|
||||||
!@closing.nil? ||
|
|
||||||
@name =~ /^(img|br|hr|link|meta|area|base|basefont|
|
|
||||||
col|frame|input|isindex|param)$/ox
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns a textual representation of the node
|
|
||||||
def to_s
|
|
||||||
if @closing == :close
|
|
||||||
"</#{@name}>"
|
|
||||||
else
|
|
||||||
s = "<#{@name}"
|
|
||||||
@attributes.each do |k,v|
|
|
||||||
s << " #{k}"
|
|
||||||
s << "=\"#{v}\"" if String === v
|
|
||||||
end
|
|
||||||
s << " /" if @closing == :self
|
|
||||||
s << ">"
|
|
||||||
@children.each { |child| s << child.to_s }
|
|
||||||
s << "</#{@name}>" if @closing != :self && !@children.empty?
|
|
||||||
s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# If either the node or any of its children meet the given conditions, the
|
|
||||||
# matching node is returned. Otherwise, +nil+ is returned. (See the
|
|
||||||
# description of the valid conditions in the +match+ method.)
|
|
||||||
def find(conditions)
|
|
||||||
match(conditions) && self || super
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns +true+, indicating that this node represents an HTML tag.
|
|
||||||
def tag?
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns +true+ if the node meets any of the given conditions. The
|
|
||||||
# +conditions+ parameter must be a hash of any of the following keys
|
|
||||||
# (all are optional):
|
|
||||||
#
|
|
||||||
# * <tt>:tag</tt>: the node name must match the corresponding value
|
|
||||||
# * <tt>:attributes</tt>: a hash. The node's values must match the
|
|
||||||
# corresponding values in the hash.
|
|
||||||
# * <tt>:parent</tt>: a hash. The node's parent must match the
|
|
||||||
# corresponding hash.
|
|
||||||
# * <tt>:child</tt>: a hash. At least one of the node's immediate children
|
|
||||||
# must meet the criteria described by the hash.
|
|
||||||
# * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
|
|
||||||
# meet the criteria described by the hash.
|
|
||||||
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
|
|
||||||
# must meet the criteria described by the hash.
|
|
||||||
# * <tt>: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.
|
|
||||||
#
|
|
||||||
# Conditions are matched using the following algorithm:
|
|
||||||
#
|
|
||||||
# * if the condition is a string, it must be a substring of the value.
|
|
||||||
# * if the condition is a regexp, it must match the value.
|
|
||||||
# * if the condition is a number, the value must match number.to_s.
|
|
||||||
# * if the condition is +true+, the value must not be +nil+.
|
|
||||||
# * if the condition is +false+ or +nil+, the value must be +nil+.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# # test if the node is a "span" tag
|
|
||||||
# node.match tag: "span"
|
|
||||||
#
|
|
||||||
# # test if the node's parent is a "div"
|
|
||||||
# node.match parent: { tag: "div" }
|
|
||||||
#
|
|
||||||
# # test if any of the node's ancestors are "table" tags
|
|
||||||
# node.match ancestor: { tag: "table" }
|
|
||||||
#
|
|
||||||
# # test if any of the node's immediate children are "em" tags
|
|
||||||
# node.match child: { tag: "em" }
|
|
||||||
#
|
|
||||||
# # test if any of the node's descendants are "strong" tags
|
|
||||||
# node.match descendant: { tag: "strong" }
|
|
||||||
#
|
|
||||||
# # test if the node has between 2 and 4 span tags as immediate children
|
|
||||||
# node.match children: { count: 2..4, only: { tag: "span" } }
|
|
||||||
#
|
|
||||||
# # get funky: test to see if the node is a "div", has a "ul" ancestor
|
|
||||||
# # and an "li" parent (with "class" = "enum"), and whether or not it has
|
|
||||||
# # a "span" descendant that contains # text matching /hello world/:
|
|
||||||
# node.match tag: "div",
|
|
||||||
# ancestor: { tag: "ul" },
|
|
||||||
# parent: { tag: "li",
|
|
||||||
# attributes: { class: "enum" } },
|
|
||||||
# descendant: { tag: "span",
|
|
||||||
# child: /hello world/ }
|
|
||||||
def match(conditions)
|
|
||||||
conditions = validate_conditions(conditions)
|
|
||||||
# check content of child nodes
|
|
||||||
if conditions[:content]
|
|
||||||
if children.empty?
|
|
||||||
return false unless match_condition("", conditions[:content])
|
|
||||||
else
|
|
||||||
return false unless children.find { |child| child.match(conditions[:content]) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# test the name
|
|
||||||
return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
|
|
||||||
|
|
||||||
# test attributes
|
|
||||||
(conditions[:attributes] || {}).each do |key, value|
|
|
||||||
return false unless match_condition(self[key], value)
|
|
||||||
end
|
|
||||||
|
|
||||||
# test parent
|
|
||||||
return false unless parent.match(conditions[:parent]) if conditions[:parent]
|
|
||||||
|
|
||||||
# test children
|
|
||||||
return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
|
|
||||||
|
|
||||||
# test ancestors
|
|
||||||
if conditions[:ancestor]
|
|
||||||
return false unless catch :found do
|
|
||||||
p = self
|
|
||||||
throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# test descendants
|
|
||||||
if conditions[:descendant]
|
|
||||||
return false unless children.find do |child|
|
|
||||||
# test the child
|
|
||||||
child.match(conditions[:descendant]) ||
|
|
||||||
# test the child's descendants
|
|
||||||
child.match(:descendant => conditions[:descendant])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# count children
|
|
||||||
if opts = conditions[:children]
|
|
||||||
matches = children.select do |c|
|
|
||||||
(c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
|
|
||||||
end
|
|
||||||
|
|
||||||
matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
|
|
||||||
opts.each do |key, value|
|
|
||||||
next if key == :only
|
|
||||||
case key
|
|
||||||
when :count
|
|
||||||
if Integer === value
|
|
||||||
return false if matches.length != value
|
|
||||||
else
|
|
||||||
return false unless value.include?(matches.length)
|
|
||||||
end
|
|
||||||
when :less_than
|
|
||||||
return false unless matches.length < value
|
|
||||||
when :greater_than
|
|
||||||
return false unless matches.length > value
|
|
||||||
else raise "unknown count condition #{key}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# test siblings
|
|
||||||
if conditions[:sibling] || conditions[:before] || conditions[:after]
|
|
||||||
siblings = parent ? parent.children : []
|
|
||||||
self_index = siblings.index(self)
|
|
||||||
|
|
||||||
if conditions[:sibling]
|
|
||||||
return false unless siblings.detect do |s|
|
|
||||||
s != self && s.match(conditions[:sibling])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if conditions[:before]
|
|
||||||
return false unless siblings[self_index+1..-1].detect do |s|
|
|
||||||
s != self && s.match(conditions[:before])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if conditions[:after]
|
|
||||||
return false unless siblings[0,self_index].detect do |s|
|
|
||||||
s != self && s.match(conditions[:after])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(node)
|
|
||||||
return false unless super
|
|
||||||
return false unless closing == node.closing && self.name == node.name
|
|
||||||
attributes == node.attributes
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
# Match the given value to the given condition.
|
|
||||||
def match_condition(value, condition)
|
|
||||||
case condition
|
|
||||||
when String
|
|
||||||
value && value == condition
|
|
||||||
when Regexp
|
|
||||||
value && value.match(condition)
|
|
||||||
when Numeric
|
|
||||||
value == condition.to_s
|
|
||||||
when true
|
|
||||||
!value.nil?
|
|
||||||
when false, nil
|
|
||||||
value.nil?
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,188 +0,0 @@
|
||||||
require 'set'
|
|
||||||
require 'cgi'
|
|
||||||
require 'active_support/core_ext/module/attribute_accessors'
|
|
||||||
|
|
||||||
module HTML
|
|
||||||
class Sanitizer
|
|
||||||
def sanitize(text, options = {})
|
|
||||||
validate_options(options)
|
|
||||||
return text unless sanitizeable?(text)
|
|
||||||
tokenize(text, options).join
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitizeable?(text)
|
|
||||||
!(text.nil? || text.empty? || !text.index("<"))
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
def tokenize(text, options)
|
|
||||||
tokenizer = HTML::Tokenizer.new(text)
|
|
||||||
result = []
|
|
||||||
while token = tokenizer.next
|
|
||||||
node = Node.parse(nil, 0, 0, token, false)
|
|
||||||
process_node node, result, options
|
|
||||||
end
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_node(node, result, options)
|
|
||||||
result << node.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate_options(options)
|
|
||||||
if options[:tags] && !options[:tags].is_a?(Enumerable)
|
|
||||||
raise ArgumentError, "You should pass :tags as an Enumerable"
|
|
||||||
end
|
|
||||||
|
|
||||||
if options[:attributes] && !options[:attributes].is_a?(Enumerable)
|
|
||||||
raise ArgumentError, "You should pass :attributes as an Enumerable"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class FullSanitizer < Sanitizer
|
|
||||||
def sanitize(text, options = {})
|
|
||||||
result = super
|
|
||||||
# strip any comments, and if they have a newline at the end (ie. line with
|
|
||||||
# only a comment) strip that too
|
|
||||||
result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m)
|
|
||||||
# Recurse - handle all dirty nested tags
|
|
||||||
result == text ? result : sanitize(result, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_node(node, result, options)
|
|
||||||
result << node.to_s if node.class == HTML::Text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class LinkSanitizer < FullSanitizer
|
|
||||||
cattr_accessor :included_tags, :instance_writer => false
|
|
||||||
self.included_tags = Set.new(%w(a href))
|
|
||||||
|
|
||||||
def sanitizeable?(text)
|
|
||||||
!(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
def process_node(node, result, options)
|
|
||||||
result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class WhiteListSanitizer < Sanitizer
|
|
||||||
[:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
|
|
||||||
:allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
|
|
||||||
class_attribute attr, :instance_writer => false
|
|
||||||
end
|
|
||||||
|
|
||||||
# A regular expression of the valid characters used to separate protocols like
|
|
||||||
# the ':' in 'http://foo.com'
|
|
||||||
self.protocol_separator = /:|(�*58)|(p)|(�*3a)|(%|%)3A/i
|
|
||||||
|
|
||||||
# Specifies a Set of HTML attributes that can have URIs.
|
|
||||||
self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
|
|
||||||
|
|
||||||
# Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
|
|
||||||
# to just escaping harmless tags like <font>
|
|
||||||
self.bad_tags = Set.new(%w(script))
|
|
||||||
|
|
||||||
# Specifies the default Set of tags that the #sanitize helper will allow unscathed.
|
|
||||||
self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
|
|
||||||
sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
|
|
||||||
acronym a img blockquote del ins))
|
|
||||||
|
|
||||||
# Specifies the default Set of html attributes that the #sanitize helper will leave
|
|
||||||
# in the allowed tag.
|
|
||||||
self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
|
|
||||||
|
|
||||||
# Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
|
|
||||||
self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
|
|
||||||
feed svn urn aim rsync tag ssh sftp rtsp afs))
|
|
||||||
|
|
||||||
# Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
|
|
||||||
self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
|
|
||||||
border-color border-left-color border-right-color border-top-color clear color cursor direction display
|
|
||||||
elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
|
|
||||||
overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
|
|
||||||
speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
|
|
||||||
width))
|
|
||||||
|
|
||||||
# Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
|
|
||||||
self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
|
|
||||||
collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
|
|
||||||
nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
|
|
||||||
|
|
||||||
# Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
|
|
||||||
self.shorthand_css_properties = Set.new(%w(background border margin padding))
|
|
||||||
|
|
||||||
# Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
|
|
||||||
def sanitize_css(style)
|
|
||||||
# disallow urls
|
|
||||||
style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
|
|
||||||
|
|
||||||
# gauntlet
|
|
||||||
if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
|
|
||||||
style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
|
|
||||||
return ''
|
|
||||||
end
|
|
||||||
|
|
||||||
clean = []
|
|
||||||
style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
|
|
||||||
if allowed_css_properties.include?(prop.downcase)
|
|
||||||
clean << prop + ': ' + val + ';'
|
|
||||||
elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
|
|
||||||
unless val.split().any? do |keyword|
|
|
||||||
!allowed_css_keywords.include?(keyword) &&
|
|
||||||
keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
|
|
||||||
end
|
|
||||||
clean << prop + ': ' + val + ';'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
clean.join(' ')
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
def tokenize(text, options)
|
|
||||||
options[:parent] = []
|
|
||||||
options[:attributes] ||= allowed_attributes
|
|
||||||
options[:tags] ||= allowed_tags
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_node(node, result, options)
|
|
||||||
result << case node
|
|
||||||
when HTML::Tag
|
|
||||||
if node.closing == :close
|
|
||||||
options[:parent].shift
|
|
||||||
else
|
|
||||||
options[:parent].unshift node.name
|
|
||||||
end
|
|
||||||
|
|
||||||
process_attributes_for node, options
|
|
||||||
|
|
||||||
options[:tags].include?(node.name) ? node : nil
|
|
||||||
else
|
|
||||||
bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_attributes_for(node, options)
|
|
||||||
return unless node.attributes
|
|
||||||
node.attributes.keys.each do |attr_name|
|
|
||||||
value = node.attributes[attr_name].to_s
|
|
||||||
|
|
||||||
if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
|
|
||||||
node.attributes.delete(attr_name)
|
|
||||||
else
|
|
||||||
node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def contains_bad_protocols?(attr_name, value)
|
|
||||||
uri_attributes.include?(attr_name) &&
|
|
||||||
(value =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,830 +0,0 @@
|
||||||
#--
|
|
||||||
# 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 separate 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>:content(string)</tt> -- Match the element only if it has <tt>string</tt>
|
|
||||||
# as its text content (ignoring leading and trailing whitespace).
|
|
||||||
# * <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 variant 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 #:nodoc:
|
|
||||||
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) }
|
|
||||||
|
|
||||||
@alternates = []
|
|
||||||
@depends = nil
|
|
||||||
|
|
||||||
# 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, [])
|
|
||||||
@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 !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
|
|
||||||
|
|
||||||
|
|
||||||
# Returns 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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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
|
|
||||||
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 reuse. 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, [])
|
|
||||||
@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
|
|
|
@ -1,107 +0,0 @@
|
||||||
require 'strscan'
|
|
||||||
|
|
||||||
module HTML #:nodoc:
|
|
||||||
|
|
||||||
# A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
|
|
||||||
# token is a string. Each string represents either "text", or an HTML element.
|
|
||||||
#
|
|
||||||
# This currently assumes valid XHTML, which means no free < or > characters.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
#
|
|
||||||
# tokenizer = HTML::Tokenizer.new(text)
|
|
||||||
# while token = tokenizer.next
|
|
||||||
# p token
|
|
||||||
# end
|
|
||||||
class Tokenizer #:nodoc:
|
|
||||||
|
|
||||||
# The current (byte) position in the text
|
|
||||||
attr_reader :position
|
|
||||||
|
|
||||||
# The current line number
|
|
||||||
attr_reader :line
|
|
||||||
|
|
||||||
# Create a new Tokenizer for the given text.
|
|
||||||
def initialize(text)
|
|
||||||
text.encode!
|
|
||||||
@scanner = StringScanner.new(text)
|
|
||||||
@position = 0
|
|
||||||
@line = 0
|
|
||||||
@current_line = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the next token in the sequence, or +nil+ if there are no more tokens in
|
|
||||||
# the stream.
|
|
||||||
def next
|
|
||||||
return nil if @scanner.eos?
|
|
||||||
@position = @scanner.pos
|
|
||||||
@line = @current_line
|
|
||||||
if @scanner.check(/<\S/)
|
|
||||||
update_current_line(scan_tag)
|
|
||||||
else
|
|
||||||
update_current_line(scan_text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Treat the text at the current position as a tag, and scan it. Supports
|
|
||||||
# comments, doctype tags, and regular tags, and ignores less-than and
|
|
||||||
# greater-than characters within quoted strings.
|
|
||||||
def scan_tag
|
|
||||||
tag = @scanner.getch
|
|
||||||
if @scanner.scan(/!--/) # comment
|
|
||||||
tag << @scanner.matched
|
|
||||||
tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
|
|
||||||
elsif @scanner.scan(/!\[CDATA\[/)
|
|
||||||
tag << @scanner.matched
|
|
||||||
tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/))
|
|
||||||
elsif @scanner.scan(/!/) # doctype
|
|
||||||
tag << @scanner.matched
|
|
||||||
tag << consume_quoted_regions
|
|
||||||
else
|
|
||||||
tag << consume_quoted_regions
|
|
||||||
end
|
|
||||||
tag
|
|
||||||
end
|
|
||||||
|
|
||||||
# Scan all text up to the next < character and return it.
|
|
||||||
def scan_text
|
|
||||||
"#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Counts the number of newlines in the text and updates the current line
|
|
||||||
# accordingly.
|
|
||||||
def update_current_line(text)
|
|
||||||
text.scan(/\r?\n/) { @current_line += 1 }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Skips over quoted strings, so that less-than and greater-than characters
|
|
||||||
# within the strings are ignored.
|
|
||||||
def consume_quoted_regions
|
|
||||||
text = ""
|
|
||||||
loop do
|
|
||||||
match = @scanner.scan_until(/['"<>]/) or break
|
|
||||||
|
|
||||||
delim = @scanner.matched
|
|
||||||
if delim == "<"
|
|
||||||
match = match.chop
|
|
||||||
@scanner.pos -= 1
|
|
||||||
end
|
|
||||||
|
|
||||||
text << match
|
|
||||||
break if delim == "<" || delim == ">"
|
|
||||||
|
|
||||||
# consume the quoted region
|
|
||||||
while match = @scanner.scan_until(/[\\#{delim}]/)
|
|
||||||
text << match
|
|
||||||
break if @scanner.matched == delim
|
|
||||||
break if @scanner.eos?
|
|
||||||
text << @scanner.getch # skip the escaped character
|
|
||||||
end
|
|
||||||
end
|
|
||||||
text
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1,11 +0,0 @@
|
||||||
module HTML #:nodoc:
|
|
||||||
module Version #:nodoc:
|
|
||||||
|
|
||||||
MAJOR = 0
|
|
||||||
MINOR = 5
|
|
||||||
TINY = 3
|
|
||||||
|
|
||||||
STRING = [ MAJOR, MINOR, TINY ].join(".")
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -254,7 +254,7 @@ class AtomFeedTest < ActionController::TestCase
|
||||||
def test_self_url_should_default_to_current_request_url
|
def test_self_url_should_default_to_current_request_url
|
||||||
with_restful_routing(:scrolls) do
|
with_restful_routing(:scrolls) do
|
||||||
get :index, :id => "defaults"
|
get :index, :id => "defaults"
|
||||||
assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]"
|
assert_select "link[rel=self][href=\"http://www.nextangle.com/scrolls?id=defaults\"]"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -318,22 +318,22 @@ class AtomFeedTest < ActionController::TestCase
|
||||||
with_restful_routing(:scrolls) do
|
with_restful_routing(:scrolls) do
|
||||||
get :index, :id => "feed_with_xhtml_content"
|
get :index, :id => "feed_with_xhtml_content"
|
||||||
assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
|
assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
|
||||||
assert_select "summary div p", :text => "Something Boring"
|
assert_select "summary", :text => /Something Boring/
|
||||||
assert_select "summary div p", :text => "after 2"
|
assert_select "summary", :text => /after 2/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_feed_entry_type_option_default_to_text_html
|
def test_feed_entry_type_option_default_to_text_html
|
||||||
with_restful_routing(:scrolls) do
|
with_restful_routing(:scrolls) do
|
||||||
get :index, :id => 'defaults'
|
get :index, :id => 'defaults'
|
||||||
assert_select "entry link[rel=alternate][type=text/html]"
|
assert_select "entry link[rel=alternate][type=\"text/html\"]"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_feed_entry_type_option_specified
|
def test_feed_entry_type_option_specified
|
||||||
with_restful_routing(:scrolls) do
|
with_restful_routing(:scrolls) do
|
||||||
get :index, :id => 'entry_type_options'
|
get :index, :id => 'entry_type_options'
|
||||||
assert_select "entry link[rel=alternate][type=text/xml]"
|
assert_select "entry link[rel=alternate][type=\"text/xml\"]"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1652,9 +1652,9 @@ class DateHelperTest < ActionView::TestCase
|
||||||
concat f.date_select(:written_on)
|
concat f.date_select(:written_on)
|
||||||
end
|
end
|
||||||
|
|
||||||
expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
|
expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
|
||||||
expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
|
expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
|
||||||
expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
|
expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
|
||||||
|
|
||||||
assert_dom_equal(expected, output_buffer)
|
assert_dom_equal(expected, output_buffer)
|
||||||
end
|
end
|
||||||
|
@ -1668,9 +1668,9 @@ class DateHelperTest < ActionView::TestCase
|
||||||
concat f.date_select(:written_on)
|
concat f.date_select(:written_on)
|
||||||
end
|
end
|
||||||
|
|
||||||
expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
|
expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
|
||||||
expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
|
expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
|
||||||
expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
|
expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
|
||||||
|
|
||||||
assert_dom_equal(expected, output_buffer)
|
assert_dom_equal(expected, output_buffer)
|
||||||
end
|
end
|
||||||
|
@ -1684,9 +1684,10 @@ class DateHelperTest < ActionView::TestCase
|
||||||
concat f.date_select(:written_on)
|
concat f.date_select(:written_on)
|
||||||
end
|
end
|
||||||
|
|
||||||
expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
|
|
||||||
expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
|
expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
|
||||||
expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
|
expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
|
||||||
|
expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
|
||||||
|
|
||||||
assert_dom_equal(expected, output_buffer)
|
assert_dom_equal(expected, output_buffer)
|
||||||
end
|
end
|
||||||
|
@ -2374,11 +2375,11 @@ class DateHelperTest < ActionView::TestCase
|
||||||
concat f.datetime_select(:updated_at, {}, :class => 'selector')
|
concat f.datetime_select(:updated_at, {}, :class => 'selector')
|
||||||
end
|
end
|
||||||
|
|
||||||
expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
|
expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n}
|
||||||
expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
|
expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n}
|
||||||
expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
|
expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n}
|
||||||
expected << " — <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
|
expected << %{ — <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n}
|
||||||
expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
|
expected << %{ : <select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option selected="selected" value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n}
|
||||||
|
|
||||||
assert_dom_equal expected, output_buffer
|
assert_dom_equal expected, output_buffer
|
||||||
end
|
end
|
||||||
|
|
|
@ -185,8 +185,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
p.collection_radio_buttons :category_id, collection, :id, :name
|
p.collection_radio_buttons :category_id, collection, :id, :name
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_select 'input#post_category_id_1[type=radio][value=1]'
|
assert_select 'input#post_category_id_1[type=radio][value="1"]'
|
||||||
assert_select 'input#post_category_id_2[type=radio][value=2]'
|
assert_select 'input#post_category_id_2[type=radio][value="2"]'
|
||||||
|
|
||||||
assert_select 'label[for=post_category_id_1]', 'Category 1'
|
assert_select 'label[for=post_category_id_1]', 'Category 1'
|
||||||
assert_select 'label[for=post_category_id_2]', 'Category 2'
|
assert_select 'label[for=post_category_id_2]', 'Category 2'
|
||||||
|
@ -203,36 +203,36 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :id, :name
|
with_collection_check_boxes :user, :category_ids, collection, :id, :name
|
||||||
|
|
||||||
assert_select 'input#user_category_ids_1[type=checkbox][value=1]'
|
assert_select 'input#user_category_ids_1[type=checkbox][value="1"]'
|
||||||
assert_select 'input#user_category_ids_2[type=checkbox][value=2]'
|
assert_select 'input#user_category_ids_2[type=checkbox][value="2"]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
|
test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
|
||||||
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :id, :name
|
with_collection_check_boxes :user, :category_ids, collection, :id, :name
|
||||||
|
|
||||||
assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
|
assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes generates a hidden field using the given :name in :html_options' do
|
test 'collection check boxes generates a hidden field using the given :name in :html_options' do
|
||||||
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
|
with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
|
||||||
|
|
||||||
assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1
|
assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", :count => 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes generates a hidden field with index if it was provided' do
|
test 'collection check boxes generates a hidden field with index if it was provided' do
|
||||||
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
|
with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
|
||||||
|
|
||||||
assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1
|
assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
|
test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
|
||||||
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
|
with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
|
||||||
|
|
||||||
assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 0
|
assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
|
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
|
||||||
|
@ -260,8 +260,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
|
collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
|
||||||
with_collection_check_boxes :user, :active, collection, :first, :second
|
with_collection_check_boxes :user, :active, collection, :first, :second
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1].foo'
|
assert_select 'input[type=checkbox][value="1"].foo'
|
||||||
assert_select 'input[type=checkbox][value=2].bar'
|
assert_select 'input[type=checkbox][value="2"].bar'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes sets the label class defined inside the block' do
|
test 'collection check boxes sets the label class defined inside the block' do
|
||||||
|
@ -286,27 +286,27 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][checked=checked]'
|
assert_select 'input[type=checkbox][value="1"][checked=checked]'
|
||||||
assert_select 'input[type=checkbox][value=3][checked=checked]'
|
assert_select 'input[type=checkbox][value="3"][checked=checked]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][checked=checked]'
|
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts selected string values as :checked option' do
|
test 'collection check boxes accepts selected string values as :checked option' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][checked=checked]'
|
assert_select 'input[type=checkbox][value="1"][checked=checked]'
|
||||||
assert_select 'input[type=checkbox][value=3][checked=checked]'
|
assert_select 'input[type=checkbox][value="3"][checked=checked]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][checked=checked]'
|
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts a single checked value' do
|
test 'collection check boxes accepts a single checked value' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=3][checked=checked]'
|
assert_select 'input[type=checkbox][value="3"][checked=checked]'
|
||||||
assert_no_select 'input[type=checkbox][value=1][checked=checked]'
|
assert_no_select 'input[type=checkbox][value="1"][checked=checked]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][checked=checked]'
|
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts selected values as :checked option and override the model values' do
|
test 'collection check boxes accepts selected values as :checked option and override the model values' do
|
||||||
|
@ -317,71 +317,71 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
|
p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][checked=checked]'
|
assert_select 'input[type=checkbox][value="1"][checked=checked]'
|
||||||
assert_select 'input[type=checkbox][value=3][checked=checked]'
|
assert_select 'input[type=checkbox][value="3"][checked=checked]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][checked=checked]'
|
assert_no_select 'input[type=checkbox][value="2"][checked=checked]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts multiple disabled items' do
|
test 'collection check boxes accepts multiple disabled items' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][disabled=disabled]'
|
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
|
||||||
assert_select 'input[type=checkbox][value=3][disabled=disabled]'
|
assert_select 'input[type=checkbox][value="3"][disabled=disabled]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
|
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts single disabled item' do
|
test 'collection check boxes accepts single disabled item' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][disabled=disabled]'
|
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
|
||||||
assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
|
assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
|
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts a proc to disabled items' do
|
test 'collection check boxes accepts a proc to disabled items' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][disabled=disabled]'
|
assert_select 'input[type=checkbox][value="1"][disabled=disabled]'
|
||||||
assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
|
assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
|
assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts multiple readonly items' do
|
test 'collection check boxes accepts multiple readonly items' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][readonly=readonly]'
|
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
|
||||||
assert_select 'input[type=checkbox][value=3][readonly=readonly]'
|
assert_select 'input[type=checkbox][value="3"][readonly=readonly]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
|
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts single readonly item' do
|
test 'collection check boxes accepts single readonly item' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][readonly=readonly]'
|
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
|
||||||
assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
|
assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
|
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts a proc to readonly items' do
|
test 'collection check boxes accepts a proc to readonly items' do
|
||||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
|
||||||
|
|
||||||
assert_select 'input[type=checkbox][value=1][readonly=readonly]'
|
assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
|
||||||
assert_no_select 'input[type=checkbox][value=3][readonly=readonly]'
|
assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
|
||||||
assert_no_select 'input[type=checkbox][value=2][readonly=readonly]'
|
assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes accepts html options' do
|
test 'collection check boxes accepts html options' do
|
||||||
collection = [[1, 'Category 1'], [2, 'Category 2']]
|
collection = [[1, 'Category 1'], [2, 'Category 2']]
|
||||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
|
with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
|
||||||
|
|
||||||
assert_select 'input.check[type=checkbox][value=1]'
|
assert_select 'input.check[type=checkbox][value="1"]'
|
||||||
assert_select 'input.check[type=checkbox][value=2]'
|
assert_select 'input.check[type=checkbox][value="2"]'
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'collection check boxes with fields for' do
|
test 'collection check boxes with fields for' do
|
||||||
|
@ -390,8 +390,8 @@ class FormCollectionsHelperTest < ActionView::TestCase
|
||||||
p.collection_check_boxes :category_ids, collection, :id, :name
|
p.collection_check_boxes :category_ids, collection, :id, :name
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_select 'input#post_category_ids_1[type=checkbox][value=1]'
|
assert_select 'input#post_category_ids_1[type=checkbox][value="1"]'
|
||||||
assert_select 'input#post_category_ids_2[type=checkbox][value=2]'
|
assert_select 'input#post_category_ids_2[type=checkbox][value="2"]'
|
||||||
|
|
||||||
assert_select 'label[for=post_category_ids_1]', 'Category 1'
|
assert_select 'label[for=post_category_ids_1]', 'Category 1'
|
||||||
assert_select 'label[for=post_category_ids_2]', 'Category 2'
|
assert_select 'label[for=post_category_ids_2]', 'Category 2'
|
||||||
|
|
|
@ -632,6 +632,6 @@ class FormTagHelperTest < ActionView::TestCase
|
||||||
private
|
private
|
||||||
|
|
||||||
def root_elem(rendered_content)
|
def root_elem(rendered_content)
|
||||||
HTML::Document.new(rendered_content).root.children[0]
|
Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class CDATANodeTest < ActiveSupport::TestCase
|
|
||||||
def setup
|
|
||||||
@node = HTML::CDATA.new(nil, 0, 0, "<p>howdy</p>")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_to_s
|
|
||||||
assert_equal "<![CDATA[<p>howdy</p>]]>", @node.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_content
|
|
||||||
assert_equal "<p>howdy</p>", @node.content
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,148 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class DocumentTest < ActiveSupport::TestCase
|
|
||||||
def test_handle_doctype
|
|
||||||
doc = nil
|
|
||||||
assert_nothing_raised do
|
|
||||||
doc = HTML::Document.new <<-HTML.strip
|
|
||||||
<!DOCTYPE "blah" "blah" "blah">
|
|
||||||
<html>
|
|
||||||
</html>
|
|
||||||
HTML
|
|
||||||
end
|
|
||||||
assert_equal 3, doc.root.children.length
|
|
||||||
assert_equal %{<!DOCTYPE "blah" "blah" "blah">}, doc.root.children[0].content
|
|
||||||
assert_match %r{\s+}m, doc.root.children[1].content
|
|
||||||
assert_equal "html", doc.root.children[2].name
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_img
|
|
||||||
doc = HTML::Document.new <<-HTML.strip
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<p><img src="hello.gif"></p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML
|
|
||||||
assert doc.find(:tag=>"img", :attributes=>{"src"=>"hello.gif"})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_all
|
|
||||||
doc = HTML::Document.new <<-HTML.strip
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<p class="test"><img src="hello.gif"></p>
|
|
||||||
<div class="foo">
|
|
||||||
<p class="test">something</p>
|
|
||||||
<p>here is <em class="test">more</em></p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML
|
|
||||||
all = doc.find_all :attributes => { :class => "test" }
|
|
||||||
assert_equal 3, all.length
|
|
||||||
assert_equal [ "p", "p", "em" ], all.map { |n| n.name }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_with_text
|
|
||||||
doc = HTML::Document.new <<-HTML.strip
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<p>Some text</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML
|
|
||||||
assert doc.find(:content => "Some text")
|
|
||||||
assert doc.find(:tag => "p", :child => { :content => "Some text" })
|
|
||||||
assert doc.find(:tag => "p", :child => "Some text")
|
|
||||||
assert doc.find(:tag => "p", :content => "Some text")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_xml
|
|
||||||
assert_nothing_raised { HTML::Document.new("<tags><tag/></tags>", true, true) }
|
|
||||||
assert_nothing_raised { HTML::Document.new("<outer><link>something</link></outer>", true, true) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_document
|
|
||||||
doc = HTML::Document.new(<<-HTML)
|
|
||||||
<div>
|
|
||||||
<h2>blah</h2>
|
|
||||||
<table>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
HTML
|
|
||||||
assert_not_nil doc.find(:tag => "div", :children => { :count => 1, :only => { :tag => "table" } })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_nesting_nothing_to_s
|
|
||||||
doc = HTML::Document.new("<tag></tag>")
|
|
||||||
assert_equal "<tag></tag>", doc.root.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_nesting_space_to_s
|
|
||||||
doc = HTML::Document.new("<tag> </tag>")
|
|
||||||
assert_equal "<tag> </tag>", doc.root.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_nesting_text_to_s
|
|
||||||
doc = HTML::Document.new("<tag>text</tag>")
|
|
||||||
assert_equal "<tag>text</tag>", doc.root.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_nesting_tag_to_s
|
|
||||||
doc = HTML::Document.new("<tag><nested /></tag>")
|
|
||||||
assert_equal "<tag><nested /></tag>", doc.root.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_cdata
|
|
||||||
doc = HTML::Document.new(<<-HTML)
|
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
|
||||||
<head>
|
|
||||||
<title><![CDATA[<br>]]></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>this document has <br> for a title</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
HTML
|
|
||||||
|
|
||||||
assert_nil doc.find(:tag => "title", :descendant => { :tag => "br" })
|
|
||||||
assert doc.find(:tag => "title", :child => "<br>")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_empty_tag
|
|
||||||
doc = HTML::Document.new("<div id='map'></div>")
|
|
||||||
assert_nil doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /./)
|
|
||||||
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /\A\Z/)
|
|
||||||
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /^$/)
|
|
||||||
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "")
|
|
||||||
assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_invalid_document
|
|
||||||
assert_nothing_raised do
|
|
||||||
HTML::Document.new("<html>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</html>")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_invalid_document_raises_exception_when_strict
|
|
||||||
assert_raise RuntimeError do
|
|
||||||
HTML::Document.new("<html>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</html>", true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1,89 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class NodeTest < ActiveSupport::TestCase
|
|
||||||
|
|
||||||
class MockNode
|
|
||||||
def initialize(matched, value)
|
|
||||||
@matched = matched
|
|
||||||
@value = value
|
|
||||||
end
|
|
||||||
|
|
||||||
def find(conditions)
|
|
||||||
@matched && self
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
@value.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def setup
|
|
||||||
@node = HTML::Node.new("parent")
|
|
||||||
@node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match
|
|
||||||
assert !@node.match("foo")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag
|
|
||||||
assert !@node.tag?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_to_s
|
|
||||||
assert_equal "1twothree", @node.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find
|
|
||||||
assert_equal "two", @node.find('blah').to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_strict
|
|
||||||
s = "<b foo='hello'' bar='baz'>"
|
|
||||||
assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_relaxed
|
|
||||||
s = "<b foo='hello'' bar='baz'>"
|
|
||||||
node = nil
|
|
||||||
assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
|
|
||||||
assert node.attributes.has_key?("foo")
|
|
||||||
assert !node.attributes.has_key?("bar")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_to_s_with_boolean_attrs
|
|
||||||
s = "<b foo bar>"
|
|
||||||
node = HTML::Node.parse(nil,0,0,s)
|
|
||||||
assert node.attributes.has_key?("foo")
|
|
||||||
assert node.attributes.has_key?("bar")
|
|
||||||
assert "<b foo bar>", node.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_with_unclosed_tag
|
|
||||||
s = "<span onmouseover='bang'"
|
|
||||||
node = nil
|
|
||||||
assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
|
|
||||||
assert node.attributes.has_key?("onmouseover")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_with_valid_cdata_section
|
|
||||||
s = "<![CDATA[<span>contents</span>]]>"
|
|
||||||
node = nil
|
|
||||||
assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
|
|
||||||
assert_kind_of HTML::CDATA, node
|
|
||||||
assert_equal '<span>contents</span>', node.content
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_strict_with_unterminated_cdata_section
|
|
||||||
s = "<![CDATA[neverending..."
|
|
||||||
assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_parse_relaxed_with_unterminated_cdata_section
|
|
||||||
s = "<![CDATA[neverending..."
|
|
||||||
node = nil
|
|
||||||
assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
|
|
||||||
assert_kind_of HTML::CDATA, node
|
|
||||||
assert_equal 'neverending...', node.content
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,330 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class SanitizerTest < ActionController::TestCase
|
|
||||||
def setup
|
|
||||||
@sanitizer = nil # used by assert_sanitizer
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_strip_tags_with_quote
|
|
||||||
sanitizer = HTML::FullSanitizer.new
|
|
||||||
string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
|
|
||||||
|
|
||||||
assert_equal ' hi', sanitizer.sanitize(string)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_strip_tags
|
|
||||||
sanitizer = HTML::FullSanitizer.new
|
|
||||||
assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))
|
|
||||||
assert_equal("<<", sanitizer.sanitize("<<<bad html>"))
|
|
||||||
assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me"))
|
|
||||||
assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
|
|
||||||
assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
|
|
||||||
assert_equal("This is a test.", sanitizer.sanitize("This is a test."))
|
|
||||||
assert_equal(
|
|
||||||
%{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize(
|
|
||||||
%{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
|
|
||||||
assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.")
|
|
||||||
assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.")
|
|
||||||
assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...")
|
|
||||||
[nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) }
|
|
||||||
assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_strip_links
|
|
||||||
sanitizer = HTML::LinkSanitizer.new
|
|
||||||
assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me")
|
|
||||||
assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
|
|
||||||
assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
|
|
||||||
assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
|
|
||||||
assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>")
|
|
||||||
assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
|
|
||||||
assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
|
|
||||||
|
|
||||||
assert_equal "<a<a", sanitizer.sanitize("<a<a")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_form
|
|
||||||
assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ''
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_plaintext
|
|
||||||
raw = "<plaintext><span>foo</span></plaintext>"
|
|
||||||
assert_sanitized raw, "<span>foo</span>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_script
|
|
||||||
assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_js_handlers
|
|
||||||
raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
|
|
||||||
assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_javascript_href
|
|
||||||
raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
|
|
||||||
assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_sanitize_image_src
|
|
||||||
raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
|
|
||||||
assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>}
|
|
||||||
end
|
|
||||||
|
|
||||||
HTML::WhiteListSanitizer.allowed_tags.each do |tag_name|
|
|
||||||
define_method "test_should_allow_#{tag_name}_tag" do
|
|
||||||
assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_allow_anchors
|
|
||||||
assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
|
|
||||||
end
|
|
||||||
|
|
||||||
# RFC 3986, sec 4.2
|
|
||||||
def test_allow_colons_in_path_component
|
|
||||||
assert_sanitized("<a href=\"./this:that\">foo</a>")
|
|
||||||
end
|
|
||||||
|
|
||||||
%w(src width height alt).each do |img_attr|
|
|
||||||
define_method "test_should_allow_image_#{img_attr}_attribute" do
|
|
||||||
assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_handle_non_html
|
|
||||||
assert_sanitized 'abc'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_handle_blank_text
|
|
||||||
assert_sanitized nil
|
|
||||||
assert_sanitized ''
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_allow_custom_tags
|
|
||||||
text = "<u>foo</u>"
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
assert_equal(text, sanitizer.sanitize(text, :tags => %w(u)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_allow_only_custom_tags
|
|
||||||
text = "<u>foo</u> with <i>bar</i>"
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u)))
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_allow_custom_tags_with_attributes
|
|
||||||
text = %(<blockquote cite="http://example.com/">foo</blockquote>)
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
assert_equal(text, sanitizer.sanitize(text))
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_allow_custom_tags_with_custom_attributes
|
|
||||||
text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo']))
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_raise_argument_error_if_tags_is_not_enumerable
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
e = assert_raise(ArgumentError) do
|
|
||||||
sanitizer.sanitize('', :tags => 'foo')
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "You should pass :tags as an Enumerable", e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_raise_argument_error_if_attributes_is_not_enumerable
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
e = assert_raise(ArgumentError) do
|
|
||||||
sanitizer.sanitize('', :attributes => 'foo')
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal "You should pass :attributes as an Enumerable", e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
[%w(img src), %w(a href)].each do |(tag, attr)|
|
|
||||||
define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
|
|
||||||
assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_flag_bad_protocols
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
%w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
|
|
||||||
assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_accept_good_protocols_ignoring_case
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
|
|
||||||
assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_accept_good_protocols_ignoring_space
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
|
|
||||||
assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_accept_good_protocols
|
|
||||||
sanitizer = HTML::WhiteListSanitizer.new
|
|
||||||
HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
|
|
||||||
assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_reject_hex_codes_in_protocol
|
|
||||||
assert_sanitized %(<a href="%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29">1</a>), "<a>1</a>"
|
|
||||||
assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_block_script_tag
|
|
||||||
assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
|
|
||||||
end
|
|
||||||
|
|
||||||
[%(<IMG SRC="javascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC=javascript:alert('XSS')>),
|
|
||||||
%(<IMG SRC=JaVaScRiPt:alert('XSS')>),
|
|
||||||
%(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
|
|
||||||
%(<IMG SRC=javascript:alert("XSS")>),
|
|
||||||
%(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
|
|
||||||
%(<IMG SRC=javascript:alert('XSS')>),
|
|
||||||
%(<IMG SRC=javascript:alert('XSS')>),
|
|
||||||
%(<IMG SRC=javascript:alert('XSS')>),
|
|
||||||
%(<IMG SRC="jav\tascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC="jav	ascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC="jav
ascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC="  javascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC="javascript:alert('XSS');">),
|
|
||||||
%(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
|
|
||||||
define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
|
|
||||||
assert_sanitized img_hack, "<img>"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_tag_broken_up_by_null
|
|
||||||
assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_invalid_script_tag
|
|
||||||
assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_script_tag_with_multiple_open_brackets
|
|
||||||
assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "<"
|
|
||||||
assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(<a)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_unclosed_script
|
|
||||||
assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_half_open_scripts
|
|
||||||
assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_not_fall_for_ridiculous_hack
|
|
||||||
img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
|
|
||||||
assert_sanitized img_hack, "<img>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_attributes
|
|
||||||
assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_illegal_style_properties
|
|
||||||
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
|
|
||||||
expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
|
|
||||||
assert_equal expected, sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_with_trailing_space
|
|
||||||
raw = "display:block; "
|
|
||||||
expected = "display: block;"
|
|
||||||
assert_equal expected, sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_xul_style_attributes
|
|
||||||
raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
|
|
||||||
assert_equal '', sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_invalid_tag_names
|
|
||||||
assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
|
|
||||||
assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_invalid_tag_names_in_single_tags
|
|
||||||
assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_img_dynsrc_lowsrc
|
|
||||||
assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_div_background_image_unicode_encoded
|
|
||||||
raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
|
|
||||||
assert_equal '', sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_div_style_expression
|
|
||||||
raw = %(width: expression(alert('XSS'));)
|
|
||||||
assert_equal '', sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_across_newlines
|
|
||||||
raw = %(\nwidth:\nexpression(alert('XSS'));\n)
|
|
||||||
assert_equal '', sanitize_css(raw)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_img_vbscript
|
|
||||||
assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_cdata_section
|
|
||||||
assert_sanitized "<![CDATA[<span>section</span>]]>", "<![CDATA[<span>section</span>]]>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_unterminated_cdata_section
|
|
||||||
assert_sanitized "<![CDATA[<span>neverending...", "<![CDATA[<span>neverending...]]>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_not_mangle_urls_with_ampersand
|
|
||||||
assert_sanitized %{<a href=\"http://www.domain.com?var1=1&var2=2\">my link</a>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_should_sanitize_neverending_attribute
|
|
||||||
assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_x03a
|
|
||||||
assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>"
|
|
||||||
assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>"
|
|
||||||
assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">)
|
|
||||||
assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>"
|
|
||||||
assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>"
|
|
||||||
assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">)
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
def assert_sanitized(input, expected = nil)
|
|
||||||
@sanitizer ||= HTML::WhiteListSanitizer.new
|
|
||||||
if input
|
|
||||||
assert_dom_equal expected || input, @sanitizer.sanitize(input)
|
|
||||||
else
|
|
||||||
assert_nil @sanitizer.sanitize(input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize_css(input)
|
|
||||||
(@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,243 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class TagNodeTest < ActiveSupport::TestCase
|
|
||||||
def test_open_without_attributes
|
|
||||||
node = tag("<tag>")
|
|
||||||
assert_equal "tag", node.name
|
|
||||||
assert_equal Hash.new, node.attributes
|
|
||||||
assert_nil node.closing
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_open_with_attributes
|
|
||||||
node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >")
|
|
||||||
assert_equal "tag1", node.name
|
|
||||||
assert_equal "hey_ho", node["foo"]
|
|
||||||
assert_equal "blah blah", node["x:bar"]
|
|
||||||
assert_equal "blah blah blah", node["baz"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_self_closing_without_attributes
|
|
||||||
node = tag("<tag/>")
|
|
||||||
assert_equal "tag", node.name
|
|
||||||
assert_equal Hash.new, node.attributes
|
|
||||||
assert_equal :self, node.closing
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_self_closing_with_attributes
|
|
||||||
node = tag("<tag a=b/>")
|
|
||||||
assert_equal "tag", node.name
|
|
||||||
assert_equal( { "a" => "b" }, node.attributes )
|
|
||||||
assert_equal :self, node.closing
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_closing_without_attributes
|
|
||||||
node = tag("</tag>")
|
|
||||||
assert_equal "tag", node.name
|
|
||||||
assert_nil node.attributes
|
|
||||||
assert_equal :close, node.closing
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_bracket_op_when_no_attributes
|
|
||||||
node = tag("</tag>")
|
|
||||||
assert_nil node["foo"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_bracket_op_when_attributes
|
|
||||||
node = tag("<tag a=b/>")
|
|
||||||
assert_equal "b", node["a"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_attributes_with_escaped_quotes
|
|
||||||
node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">")
|
|
||||||
assert_equal "b\\'c", node["a"]
|
|
||||||
assert_equal "bob \\\"float\\\"", node["b"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_to_s
|
|
||||||
node = tag("<a b=c d='f' g=\"h 'i'\" />")
|
|
||||||
node = node.to_s
|
|
||||||
assert node.include?('a')
|
|
||||||
assert node.include?('b="c"')
|
|
||||||
assert node.include?('d="f"')
|
|
||||||
assert node.include?('g="h')
|
|
||||||
assert node.include?('i')
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag
|
|
||||||
assert tag("<tag>").tag?
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_tag_as_string
|
|
||||||
assert tag("<tag>").match(:tag => "tag")
|
|
||||||
assert !tag("<tag>").match(:tag => "b")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_tag_as_regexp
|
|
||||||
assert tag("<tag>").match(:tag => /t.g/)
|
|
||||||
assert !tag("<tag>").match(:tag => /t[bqs]g/)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_attributes_as_string
|
|
||||||
t = tag("<tag a=something b=else />")
|
|
||||||
assert t.match(:attributes => {"a" => "something"})
|
|
||||||
assert t.match(:attributes => {"b" => "else"})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_attributes_as_regexp
|
|
||||||
t = tag("<tag a=something b=else />")
|
|
||||||
assert t.match(:attributes => {"a" => /^something$/})
|
|
||||||
assert t.match(:attributes => {"b" => /e.*e/})
|
|
||||||
assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_attributes_as_number
|
|
||||||
t = tag("<tag a=15 b=3.1415 />")
|
|
||||||
assert t.match(:attributes => {"a" => 15})
|
|
||||||
assert t.match(:attributes => {"b" => 3.1415})
|
|
||||||
assert t.match(:attributes => {"a" => 15, "b" => 3.1415})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_attributes_exist
|
|
||||||
t = tag("<tag a=15 b=3.1415 />")
|
|
||||||
assert t.match(:attributes => {"a" => true})
|
|
||||||
assert t.match(:attributes => {"b" => true})
|
|
||||||
assert t.match(:attributes => {"a" => true, "b" => true})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_attributes_not_exist
|
|
||||||
t = tag("<tag a=15 b=3.1415 />")
|
|
||||||
assert t.match(:attributes => {"c" => false})
|
|
||||||
assert t.match(:attributes => {"c" => nil})
|
|
||||||
assert t.match(:attributes => {"a" => true, "c" => false})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_parent_success
|
|
||||||
t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
|
|
||||||
assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_parent_fail
|
|
||||||
t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
|
|
||||||
assert !t.match(:parent => {:tag => /kafka/})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_child_success
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("<child v=john a=kelly>", t)
|
|
||||||
tag("<sib m=vaughn v=james>", t)
|
|
||||||
assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}})
|
|
||||||
assert t.match(:child => { :attributes => {"a" => "kelly"}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_child_fail
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("<child v=john a=kelly>", t)
|
|
||||||
tag("<sib m=vaughn v=james>", t)
|
|
||||||
assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}})
|
|
||||||
assert !t.match(:child => { :attributes => {"v" => false}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_ancestor_success
|
|
||||||
t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
|
|
||||||
assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}})
|
|
||||||
assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_ancestor_fail
|
|
||||||
t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
|
|
||||||
assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}})
|
|
||||||
assert !t.match(:ancestor => {:attributes => {"v" => false}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_descendant_success
|
|
||||||
tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
|
|
||||||
assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}})
|
|
||||||
assert t.match(:descendant => {:attributes => {"m" => "vaughn"}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_descendant_fail
|
|
||||||
tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
|
|
||||||
assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}})
|
|
||||||
assert !t.match(:descendant => {:attributes => {"v" => false}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_child_count
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("hello", t)
|
|
||||||
tag("<child v=john a=kelly>", t)
|
|
||||||
tag("<sib m=vaughn v=james>", t)
|
|
||||||
assert t.match(:children => { :count => 2 })
|
|
||||||
assert t.match(:children => { :count => 2..4 })
|
|
||||||
assert t.match(:children => { :less_than => 4 })
|
|
||||||
assert t.match(:children => { :greater_than => 1 })
|
|
||||||
assert !t.match(:children => { :count => 3 })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_conditions_as_strings
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
assert t.match("tag" => "tag")
|
|
||||||
assert t.match("attributes" => { "x:k" => "something" })
|
|
||||||
assert !t.match("tag" => "gat")
|
|
||||||
assert !t.match("attributes" => { "x:j" => "something" })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_attributes_as_symbols
|
|
||||||
t = tag("<child v=john a=kelly>")
|
|
||||||
assert t.match(:attributes => { :v => /oh/ })
|
|
||||||
assert t.match(:attributes => { :a => /ll/ })
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_sibling
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("hello", t)
|
|
||||||
tag("<span a=b>", t)
|
|
||||||
tag("world", t)
|
|
||||||
m = tag("<span k=r>", t)
|
|
||||||
tag("<span m=l>", t)
|
|
||||||
|
|
||||||
assert m.match(:sibling => {:tag => "span", :attributes => {:a => true}})
|
|
||||||
assert m.match(:sibling => {:tag => "span", :attributes => {:m => true}})
|
|
||||||
assert !m.match(:sibling => {:tag => "span", :attributes => {:k => true}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_sibling_before
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("hello", t)
|
|
||||||
tag("<span a=b>", t)
|
|
||||||
tag("world", t)
|
|
||||||
m = tag("<span k=r>", t)
|
|
||||||
tag("<span m=l>", t)
|
|
||||||
|
|
||||||
assert m.match(:before => {:tag => "span", :attributes => {:m => true}})
|
|
||||||
assert !m.match(:before => {:tag => "span", :attributes => {:a => true}})
|
|
||||||
assert !m.match(:before => {:tag => "span", :attributes => {:k => true}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_sibling_after
|
|
||||||
t = tag("<tag x:k='something'>")
|
|
||||||
tag("hello", t)
|
|
||||||
tag("<span a=b>", t)
|
|
||||||
tag("world", t)
|
|
||||||
m = tag("<span k=r>", t)
|
|
||||||
tag("<span m=l>", t)
|
|
||||||
|
|
||||||
assert m.match(:after => {:tag => "span", :attributes => {:a => true}})
|
|
||||||
assert !m.match(:after => {:tag => "span", :attributes => {:m => true}})
|
|
||||||
assert !m.match(:after => {:tag => "span", :attributes => {:k => true}})
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_to_s
|
|
||||||
t = tag("<b x='foo'>")
|
|
||||||
tag("hello", t)
|
|
||||||
tag("<hr />", t)
|
|
||||||
assert_equal %(<b x="foo">hello<hr /></b>), t.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def tag(content, parent=nil)
|
|
||||||
node = HTML::Node.parse(parent,0,0,content)
|
|
||||||
parent.children << node if parent
|
|
||||||
node
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,50 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class TextNodeTest < ActiveSupport::TestCase
|
|
||||||
def setup
|
|
||||||
@node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_to_s
|
|
||||||
assert_equal "hello, howdy, aloha, annyeong", @node.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_string
|
|
||||||
assert_equal @node, @node.find("hello, howdy, aloha, annyeong")
|
|
||||||
assert_equal false, @node.find("bogus")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_regexp
|
|
||||||
assert_equal @node, @node.find(/an+y/)
|
|
||||||
assert_nil @node.find(/b/)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_hash
|
|
||||||
assert_equal @node, @node.find(:content => /howdy/)
|
|
||||||
assert_nil @node.find(:content => /^howdy$/)
|
|
||||||
assert_equal false, @node.find(:content => "howdy")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_find_other
|
|
||||||
assert_nil @node.find(:hello)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_string
|
|
||||||
assert @node.match("hello, howdy, aloha, annyeong")
|
|
||||||
assert_equal false, @node.match("bogus")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_regexp
|
|
||||||
assert_not_nil @node, @node.match(/an+y/)
|
|
||||||
assert_nil @node.match(/b/)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_hash
|
|
||||||
assert_not_nil @node, @node.match(:content => "howdy")
|
|
||||||
assert_nil @node.match(:content => /^howdy$/)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_match_other
|
|
||||||
assert_nil @node.match(:hello)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,131 +0,0 @@
|
||||||
require 'abstract_unit'
|
|
||||||
|
|
||||||
class TokenizerTest < ActiveSupport::TestCase
|
|
||||||
|
|
||||||
def test_blank
|
|
||||||
tokenize ""
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_space
|
|
||||||
tokenize " "
|
|
||||||
assert_next " "
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_simple_open
|
|
||||||
tokenize "<tag>"
|
|
||||||
assert_next "<tag>"
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_simple_self_closing
|
|
||||||
tokenize "<tag />"
|
|
||||||
assert_next "<tag />"
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_simple_closing
|
|
||||||
tokenize "</tag>"
|
|
||||||
assert_next "</tag>"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_single_quoted_attribute
|
|
||||||
tokenize %{<tag a='hello'>x}
|
|
||||||
assert_next %{<tag a='hello'>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_single_quoted_attribute_with_escape
|
|
||||||
tokenize %{<tag a='hello\\''>x}
|
|
||||||
assert_next %{<tag a='hello\\''>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_double_quoted_attribute
|
|
||||||
tokenize %{<tag a="hello">x}
|
|
||||||
assert_next %{<tag a="hello">}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_double_quoted_attribute_with_escape
|
|
||||||
tokenize %{<tag a="hello\\"">x}
|
|
||||||
assert_next %{<tag a="hello\\"">}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_unquoted_attribute
|
|
||||||
tokenize %{<tag a=hello>x}
|
|
||||||
assert_next %{<tag a=hello>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_lt_char_in_attribute
|
|
||||||
tokenize %{<tag a="x < y">x}
|
|
||||||
assert_next %{<tag a="x < y">}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_tag_with_gt_char_in_attribute
|
|
||||||
tokenize %{<tag a="x > y">x}
|
|
||||||
assert_next %{<tag a="x > y">}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_doctype_tag
|
|
||||||
tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>}
|
|
||||||
assert_next %{<!DOCTYPE "blah" "blah" "blah">}
|
|
||||||
assert_next %{\n }
|
|
||||||
assert_next %{<html>}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_cdata_tag
|
|
||||||
tokenize %{<![CDATA[<br>]]>}
|
|
||||||
assert_next %{<![CDATA[<br>]]>}
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_unterminated_cdata_tag
|
|
||||||
tokenize %{<content:encoded><![CDATA[ neverending...}
|
|
||||||
assert_next %{<content:encoded>}
|
|
||||||
assert_next %{<![CDATA[ neverending...}
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_less_than_with_space
|
|
||||||
tokenize %{original < hello > world}
|
|
||||||
assert_next %{original }
|
|
||||||
assert_next %{< hello > world}
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_less_than_without_matching_greater_than
|
|
||||||
tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>}
|
|
||||||
assert_next %{hello }
|
|
||||||
assert_next %{<span onmouseover="gotcha"\n}
|
|
||||||
assert_next %{<b>}
|
|
||||||
assert_next %{foo}
|
|
||||||
assert_next %{</b>}
|
|
||||||
assert_next %{\nbar}
|
|
||||||
assert_next %{</span>}
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_unterminated_comment
|
|
||||||
tokenize %{hello <!-- neverending...}
|
|
||||||
assert_next %{hello }
|
|
||||||
assert_next %{<!-- neverending...}
|
|
||||||
assert_end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def tokenize(text)
|
|
||||||
@tokenizer = HTML::Tokenizer.new(text)
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_next(expected, message=nil)
|
|
||||||
token = @tokenizer.next
|
|
||||||
assert_equal expected, token, message
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_sequence(*expected)
|
|
||||||
assert_next expected.shift until expected.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
def assert_end(message=nil)
|
|
||||||
assert_nil @tokenizer.next, message
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,19 +1,15 @@
|
||||||
require 'abstract_unit'
|
require 'abstract_unit'
|
||||||
|
|
||||||
# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb
|
# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
|
||||||
# This tests the that the helpers hook up correctly to the sanitizer classes.
|
# This tests that the helpers hook up correctly to the sanitizer classes.
|
||||||
class SanitizeHelperTest < ActionView::TestCase
|
class SanitizeHelperTest < ActionView::TestCase
|
||||||
tests ActionView::Helpers::SanitizeHelper
|
tests ActionView::Helpers::SanitizeHelper
|
||||||
|
|
||||||
def test_strip_links
|
def test_strip_links
|
||||||
assert_equal "Dont touch me", strip_links("Dont touch me")
|
assert_equal "Dont touch me", strip_links("Dont touch me")
|
||||||
assert_equal "<a<a", strip_links("<a<a")
|
|
||||||
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
|
assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
|
||||||
assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
|
|
||||||
assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
|
assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
|
||||||
assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
|
|
||||||
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
|
assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
|
||||||
assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sanitize_form
|
def test_sanitize_form
|
||||||
|
@ -22,27 +18,15 @@ class SanitizeHelperTest < ActionView::TestCase
|
||||||
|
|
||||||
def test_should_sanitize_illegal_style_properties
|
def test_should_sanitize_illegal_style_properties
|
||||||
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
|
raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
|
||||||
expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
|
expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;)
|
||||||
assert_equal expected, sanitize_css(raw)
|
assert_equal expected, sanitize_css(raw)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strip_tags
|
def test_strip_tags
|
||||||
assert_equal("<<<bad html", strip_tags("<<<bad html"))
|
|
||||||
assert_equal("<<", strip_tags("<<<bad html>"))
|
|
||||||
assert_equal("Dont touch me", strip_tags("Dont touch me"))
|
assert_equal("Dont touch me", strip_tags("Dont touch me"))
|
||||||
assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
|
assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
|
||||||
assert_equal("Weirdos", strip_tags("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
|
|
||||||
assert_equal("This is a test.", strip_tags("This is a test."))
|
|
||||||
assert_equal(
|
|
||||||
%{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags(
|
|
||||||
%{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
|
|
||||||
assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
|
assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
|
||||||
[nil, '', ' '].each do |blank|
|
|
||||||
stripped = strip_tags(blank)
|
|
||||||
assert_equal blank, stripped
|
|
||||||
end
|
|
||||||
assert_equal "", strip_tags("<script>")
|
assert_equal "", strip_tags("<script>")
|
||||||
assert_equal "something <img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_sanitize_is_marked_safe
|
def test_sanitize_is_marked_safe
|
||||||
|
|
|
@ -187,7 +187,9 @@ class TextHelperTest < ActionView::TestCase
|
||||||
"This text is not changed because we supplied an empty phrase",
|
"This text is not changed because we supplied an empty phrase",
|
||||||
highlight("This text is not changed because we supplied an empty phrase", nil)
|
highlight("This text is not changed because we supplied an empty phrase", nil)
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_highlight_pending
|
||||||
assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
|
assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class UrlHelperTest < ActiveSupport::TestCase
|
||||||
include routes.url_helpers
|
include routes.url_helpers
|
||||||
|
|
||||||
include ActionView::Helpers::JavaScriptHelper
|
include ActionView::Helpers::JavaScriptHelper
|
||||||
include ActionDispatch::Assertions::DomAssertions
|
include Rails::Dom::Testing::Assertions::DomAssertions
|
||||||
include ActionView::Context
|
include ActionView::Context
|
||||||
include RenderERBUtils
|
include RenderERBUtils
|
||||||
|
|
||||||
|
|
|
@ -605,13 +605,13 @@ end
|
||||||
|
|
||||||
Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax.
|
Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax.
|
||||||
|
|
||||||
NOTE: You may find references to `assert_tag` in other documentation, but this is now deprecated in favor of `assert_select`.
|
NOTE: You may find references to `assert_tag` in other documentation. This has been removed in 4.2. Use `assert_select` instead.
|
||||||
|
|
||||||
There are two forms of `assert_select`:
|
There are two forms of `assert_select`:
|
||||||
|
|
||||||
`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String), an expression with substitution values, or an `HTML::Selector` object.
|
`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String) or an expression with substitution values.
|
||||||
|
|
||||||
`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `HTML::Node`) and its descendants.
|
`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `Nokogiri::XML::Node` or `Nokogiri::XML::NodeSet`) and its descendants.
|
||||||
|
|
||||||
For example, you could verify the contents on the title element in your response with:
|
For example, you could verify the contents on the title element in your response with:
|
||||||
|
|
||||||
|
@ -641,7 +641,7 @@ assert_select "ol" do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/SelectorAssertions.html).
|
The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb).
|
||||||
|
|
||||||
#### Additional View-Based Assertions
|
#### Additional View-Based Assertions
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,38 @@ after_bundle do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Rails Html Sanitizer
|
||||||
|
|
||||||
|
There's a new choice for sanitizing HTML fragments in your applications. The
|
||||||
|
venerable html-scanner approach is now officially being deprecated in favor of
|
||||||
|
[`Rails Html Sanitizer`](https://github.com/rails/rails-html-sanitizer).
|
||||||
|
|
||||||
|
This means the methods `sanitize`, `sanitize_css`, `strip_tags` and
|
||||||
|
`strip_links` are backed by a new implementation.
|
||||||
|
|
||||||
|
In the next major Rails version `Rails Html Sanitizer` will be the default
|
||||||
|
sanitizer. It already is for new applications.
|
||||||
|
|
||||||
|
Include this in your Gemfile to try it out today:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gem 'rails-html-sanitizer'
|
||||||
|
```
|
||||||
|
|
||||||
|
This new sanitizer uses [Loofah](https://github.com/flavorjones/loofah) internally. Loofah in turn uses Nokogiri, which
|
||||||
|
wraps XML parsers written in both C and Java, so sanitization should be faster
|
||||||
|
no matter which Ruby version you run.
|
||||||
|
|
||||||
|
The new version updates `sanitize`, so it can take a `Loofah::Scrubber` for
|
||||||
|
powerful scrubbing.
|
||||||
|
[See some examples of scrubbers here](https://github.com/flavorjones/loofah#loofahscrubber).
|
||||||
|
|
||||||
|
Two new scrubbers have also been added: `PermitScrubber` and `TargetScrubber`.
|
||||||
|
Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more information.
|
||||||
|
|
||||||
|
The documentation for `PermitScrubber` and `TargetScrubber` explains how you
|
||||||
|
can gain complete control over when and how elements should be stripped.
|
||||||
|
|
||||||
Upgrading from Rails 4.0 to Rails 4.1
|
Upgrading from Rails 4.0 to Rails 4.1
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,11 @@ source 'https://rubygems.org'
|
||||||
# Use ActiveModel has_secure_password
|
# Use ActiveModel has_secure_password
|
||||||
# gem 'bcrypt', '~> 3.1.7'
|
# gem 'bcrypt', '~> 3.1.7'
|
||||||
|
|
||||||
|
# Use Rails Html Sanitizer for HTML sanitization
|
||||||
|
gem 'rails-html-snaitizer', github: 'rails/rails', branch: 'master'
|
||||||
|
#temporary gem until a new version of loofah is released
|
||||||
|
gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub'
|
||||||
|
|
||||||
# Use Unicorn as the app server
|
# Use Unicorn as the app server
|
||||||
# gem 'unicorn'
|
# gem 'unicorn'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue