#-- # Copyright (c) 2006 Assaf Arkin (http://labnotes.org) # Under MIT and/or CC By license. #++ require 'abstract_unit' require 'controller/fake_controllers' require 'action_controller/vendor/html-scanner' class SelectorTest < Test::Unit::TestCase # # Basic selector: element, id, class, attributes. # def test_element parse(%Q{

}) # 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{

}) # 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{

}) # 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{

}) # 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{
}) # 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{
}) # 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{

}) # 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{

}) 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{

}) 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{

}) # 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{

}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # 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{
}) # First child. select("tr:first-child") assert_equal 0, @matches.size select(":first-child") assert_equal 1, @matches.size assert_equal "thead", @matches[0].name # First of type. select("tr:first-of-type") assert_equal 1, @matches.size assert_equal "1", @matches[0].attributes["id"] select("thead:first-of-type") assert_equal 1, @matches.size assert_equal "thead", @matches[0].name select("div:first-of-type") assert_equal 0, @matches.size # Last child. select("tr:last-child") assert_equal 1, @matches.size assert_equal "4", @matches[0].attributes["id"] # Last of type. select("tr:last-of-type") assert_equal 1, @matches.size assert_equal "4", @matches[0].attributes["id"] select("thead:last-of-type") assert_equal 1, @matches.size assert_equal "thead", @matches[0].name select("div:last-of-type") assert_equal 0, @matches.size end def test_first_and_last # Only child. parse(%Q{
}) 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{
}) select("tr:only-child") assert_equal 0, @matches.size # Only of type. parse(%Q{
}) 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{
}) select("table:empty") assert_equal 0, @matches.size select("tr:empty") assert_equal 1, @matches.size parse(%Q{
}) select("div:empty") assert_equal 1, @matches.size end def test_content parse(%Q{
}) select("div:content()") assert_equal 1, @matches.size parse(%Q{
something
}) 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{

}) 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{

}) 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{

}) 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{

}) 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{

}) 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{

}) 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{

}) 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