From 56d77dc2ecfc1a391bd61a1a5165ba6c5f922b76 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 09:33:29 -0400 Subject: [PATCH 1/7] test: identify tests of XPathVisitor constructor and attributes --- test/css/test_xpath_visitor.rb | 84 ++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index 8d74ca0414d..5dd220b6a62 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -14,51 +14,55 @@ def assert_xpath(expecteds, asts) end end - it "accepts some config parameters" do - assert_equal( - Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER, - Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER).builtins, - ) - assert_equal( - Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, - Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS).builtins, - ) - assert_equal( - Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL, - Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL).builtins, - ) - assert_raises(ArgumentError) { Nokogiri::CSS::XPathVisitor.new(builtins: :not_valid) } + describe ".new" do + it "accepts some config parameters" do + assert_equal( + Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER, + Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER).builtins, + ) + assert_equal( + Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, + Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS).builtins, + ) + assert_equal( + Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL, + Nokogiri::CSS::XPathVisitor.new(builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::OPTIMAL).builtins, + ) + assert_raises(ArgumentError) { Nokogiri::CSS::XPathVisitor.new(builtins: :not_valid) } - assert_equal( - Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML, - Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML).doctype, - ) - assert_equal( - Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML4, - Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML4).doctype, - ) - assert_equal( - Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5, - Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5).doctype, - ) - assert_raises(ArgumentError) { Nokogiri::CSS::XPathVisitor.new(doctype: :not_valid) } + assert_equal( + Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML, + Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML).doctype, + ) + assert_equal( + Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML4, + Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML4).doctype, + ) + assert_equal( + Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5, + Nokogiri::CSS::XPathVisitor.new(doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5).doctype, + ) + assert_raises(ArgumentError) { Nokogiri::CSS::XPathVisitor.new(doctype: :not_valid) } - assert_equal({ "foo": "bar" }, Nokogiri::CSS::XPathVisitor.new(namespaces: { "foo": "bar" }).namespaces) + assert_equal({ "foo": "bar" }, Nokogiri::CSS::XPathVisitor.new(namespaces: { "foo": "bar" }).namespaces) - assert_equal("xxx", Nokogiri::CSS::XPathVisitor.new(prefix: "xxx").prefix) + assert_equal("xxx", Nokogiri::CSS::XPathVisitor.new(prefix: "xxx").prefix) + end end - it "exposes its configuration" do - expected = { - builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER, - doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML, - prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX, - namespaces: nil, - } - assert_equal(expected, visitor.config) - - assert_nil(visitor.namespaces) - assert_equal(Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX, visitor.prefix) + dsecribe "#config" do + it "exposes its configuration" do + expected = { + builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER, + doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::XML, + prefix: Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX, + namespaces: nil, + } + assert_equal(expected, visitor.config) + + assert_nil(visitor.namespaces) + assert_equal(Nokogiri::XML::XPath::GLOBAL_SEARCH_PREFIX, visitor.prefix) + end end it "raises an exception on single quote" do From 3374febf1d913f6e2e93ac034bd5c840ff73f974 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 13:59:12 -0400 Subject: [PATCH 2/7] test: consolidate CSS error handling tests into test_css.rb Also remove redundant coverage from test_xpath_visitor.rb --- test/css/test_css.rb | 6 ++++++ test/css/test_xpath_visitor.rb | 16 +--------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/test/css/test_css.rb b/test/css/test_css.rb index 0cb3b37f5ab..26d7af04f45 100644 --- a/test/css/test_css.rb +++ b/test/css/test_css.rb @@ -69,6 +69,12 @@ assert_raises(TypeError) { Nokogiri::CSS.xpath_for(Object.new) } assert_raises(TypeError) { Nokogiri::CSS.xpath_for(["foo", "bar"]) } end + + it "raises an exception for pseudo-classes that are not XPath Names" do + # see https://github.com/sparklemotion/nokogiri/issues/3193 + assert_raises(Nokogiri::CSS::SyntaxError) { Nokogiri::CSS.xpath_for("div:-moz-drag-over") } + assert_raises(Nokogiri::CSS::SyntaxError) { Nokogiri::CSS.xpath_for("div:-moz-drag-over()") } + end end end end diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index 5dd220b6a62..4e691891af1 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -50,7 +50,7 @@ def assert_xpath(expecteds, asts) end end - dsecribe "#config" do + describe "#config" do it "exposes its configuration" do expected = { builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER, @@ -65,14 +65,6 @@ def assert_xpath(expecteds, asts) end end - it "raises an exception on single quote" do - assert_raises(Nokogiri::CSS::SyntaxError) { parser.parse("'") } - end - - it "raises an exception on invalid CSS syntax" do - assert_raises(Nokogiri::CSS::SyntaxError) { parser.parse("a[x=]") } - end - describe "selectors" do it "* universal" do assert_xpath("//*", parser.parse("*")) @@ -407,12 +399,6 @@ def assert_xpath(expecteds, asts) assert_xpath("//*[not(@id='foo')]", parser.parse(":not(#foo)")) assert_xpath("//*[count(preceding-sibling::*)=0]", parser.parse(":first-child")) end - - it "raises an exception for pseudo-classes that are not XPath Names" do - # see https://github.com/sparklemotion/nokogiri/issues/3193 - assert_raises(Nokogiri::CSS::SyntaxError) { Nokogiri::CSS.xpath_for("div:-moz-drag-over") } - assert_raises(Nokogiri::CSS::SyntaxError) { Nokogiri::CSS.xpath_for("div:-moz-drag-over()") } - end end describe "combinators" do From 807d8516048732f393af5b19ead2501d3ff57945 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 14:17:16 -0400 Subject: [PATCH 3/7] test: clean up SelectorCache tests --- lib/nokogiri/css/selector_cache.rb | 2 +- test/css/test_parser_cache.rb | 106 ----------------------------- test/css/test_selector_cache.rb | 97 ++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 107 deletions(-) delete mode 100644 test/css/test_parser_cache.rb create mode 100644 test/css/test_selector_cache.rb diff --git a/lib/nokogiri/css/selector_cache.rb b/lib/nokogiri/css/selector_cache.rb index ca32246219f..cfd509c10b4 100644 --- a/lib/nokogiri/css/selector_cache.rb +++ b/lib/nokogiri/css/selector_cache.rb @@ -22,7 +22,7 @@ def []=(key, value) # Clear the cache def clear_cache(create_new_object = false) @mutex.synchronize do - if create_new_object + if create_new_object # used in tests to avoid 'method redefined' warnings when injecting spies @cache = {} else @cache.clear diff --git a/test/css/test_parser_cache.rb b/test/css/test_parser_cache.rb deleted file mode 100644 index afad8cd8bf5..00000000000 --- a/test/css/test_parser_cache.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -describe Nokogiri::CSS::Parser do - describe "cache" do - def setup - super - @css = "a1 > b2 > c3" - - Nokogiri::CSS::SelectorCache.clear_cache - Nokogiri::CSS::SelectorCache.class_eval do - class << @cache - alias_method :old_bracket, :[] - - def access_count - @access_count ||= 0 - end - - def [](key) - @access_count ||= 0 - @access_count += 1 - old_bracket(key) - end - end - end - end - - def teardown - Nokogiri::CSS::SelectorCache.clear_cache(true) - super - end - - [false, true].each do |cache_setting| - define_method "test_css_cache_#{cache_setting ? "true" : "false"}" do - Nokogiri::CSS.xpath_for(@css, cache: cache_setting) - Nokogiri::CSS.xpath_for(@css, cache: cache_setting) - - if cache_setting - assert_equal(1, Nokogiri::CSS::SelectorCache.class_eval { @cache.count }) - assert_equal(2, Nokogiri::CSS::SelectorCache.class_eval { @cache.access_count }) - else - assert_equal(0, Nokogiri::CSS::SelectorCache.class_eval { @cache.count }) - assert_equal(0, Nokogiri::CSS::SelectorCache.class_eval { @cache.access_count }) - end - end - end - end - - class TestCssCache < Nokogiri::TestCase - def test_enabled_cache_is_used - Nokogiri::CSS::SelectorCache.clear_cache - - css = ".foo .bar .baz" - cache = Nokogiri::CSS::SelectorCache.instance_variable_get(:@cache) - - assert_empty(cache) - Nokogiri::CSS.xpath_for(css) - refute_empty(cache) - key = cache.keys.first - - cache[key] = "this is an injected value" - assert_equal("this is an injected value", Nokogiri::CSS.xpath_for(css)) - end - - def test_disabled_cache_is_not_used - Nokogiri::CSS::SelectorCache.clear_cache - - css = ".foo .bar .baz" - cache = Nokogiri::CSS::SelectorCache.instance_variable_get(:@cache) - - assert_empty(cache) - Nokogiri::CSS.xpath_for(css, cache: false) - assert_empty(cache) - end - - def test_cache_key_on_ns_prefix_and_visitor_config - Nokogiri::CSS::SelectorCache.clear_cache - - cache = Nokogiri::CSS::SelectorCache.instance_variable_get(:@cache) - assert_empty(cache) - - Nokogiri::CSS.xpath_for("foo") - Nokogiri::CSS.xpath_for("foo", prefix: ".//") - Nokogiri::CSS.xpath_for("foo", prefix: ".//", ns: { "example" => "http://example.com/" }) - Nokogiri::CSS.xpath_for( - "foo", - visitor: Nokogiri::CSS::XPathVisitor.new( - builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, - prefix: ".//", - namespaces: { "example" => "http://example.com/" }, - ), - ) - Nokogiri::CSS.xpath_for( - "foo", - visitor: Nokogiri::CSS::XPathVisitor.new( - builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, - doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5, - prefix: ".//", - namespaces: { "example" => "http://example.com/" }, - ), - ) - assert_equal(5, cache.length) - end - end -end diff --git a/test/css/test_selector_cache.rb b/test/css/test_selector_cache.rb new file mode 100644 index 00000000000..24d11b766c6 --- /dev/null +++ b/test/css/test_selector_cache.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "helper" + +describe Nokogiri::CSS::SelectorCache do + before do + Nokogiri::CSS::SelectorCache.clear_cache + Nokogiri::CSS::SelectorCache.class_eval do + class << @cache + alias_method :old_bracket, :[] + + def access_count + @access_count ||= 0 + end + + def [](key) + @access_count ||= 0 + @access_count += 1 + old_bracket(key) + end + end + end + end + + after do + Nokogiri::CSS::SelectorCache.clear_cache(true) + end + + let(:selector_list) { "a1 > b2 > c3" } + + it "uses the cache by default" do + Nokogiri::CSS.xpath_for(selector_list) + Nokogiri::CSS.xpath_for(selector_list) + + assert_equal(1, Nokogiri::CSS::SelectorCache.class_eval { @cache.count }) + assert_equal(2, Nokogiri::CSS::SelectorCache.class_eval { @cache.access_count }) + end + + it "uses the cache when explicitly requested" do + Nokogiri::CSS.xpath_for(selector_list, cache: true) + Nokogiri::CSS.xpath_for(selector_list, cache: true) + + assert_equal(1, Nokogiri::CSS::SelectorCache.class_eval { @cache.count }) + assert_equal(2, Nokogiri::CSS::SelectorCache.class_eval { @cache.access_count }) + end + + it "does not use the cache when explicitly requested" do + Nokogiri::CSS.xpath_for(selector_list, cache: false) + Nokogiri::CSS.xpath_for(selector_list, cache: false) + + assert_equal(0, Nokogiri::CSS::SelectorCache.class_eval { @cache.count }) + assert_equal(0, Nokogiri::CSS::SelectorCache.class_eval { @cache.access_count }) + end + + it "uses the cached expressions" do + Nokogiri::CSS::SelectorCache.clear_cache + + cache = Nokogiri::CSS::SelectorCache.instance_variable_get(:@cache) + + assert_empty(cache) + Nokogiri::CSS.xpath_for(selector_list) + refute_empty(cache) + key = cache.keys.first + + cache[key] = ["this is an injected value"] + assert_equal(["this is an injected value"], Nokogiri::CSS.xpath_for(selector_list)) + end + + it "test_cache_key_on_ns_prefix_and_visitor_config" do + Nokogiri::CSS::SelectorCache.clear_cache + + cache = Nokogiri::CSS::SelectorCache.instance_variable_get(:@cache) + assert_empty(cache) + + Nokogiri::CSS.xpath_for(selector_list) + Nokogiri::CSS.xpath_for(selector_list, prefix: ".//") + Nokogiri::CSS.xpath_for(selector_list, prefix: ".//", ns: { "example" => "http://example.com/" }) + Nokogiri::CSS.xpath_for( + selector_list, + visitor: Nokogiri::CSS::XPathVisitor.new( + builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, + prefix: ".//", + namespaces: { "example" => "http://example.com/" }, + ), + ) + Nokogiri::CSS.xpath_for( + selector_list, + visitor: Nokogiri::CSS::XPathVisitor.new( + builtins: Nokogiri::CSS::XPathVisitor::BuiltinsConfig::ALWAYS, + doctype: Nokogiri::CSS::XPathVisitor::DoctypeConfig::HTML5, + prefix: ".//", + namespaces: { "example" => "http://example.com/" }, + ), + ) + assert_equal(5, cache.length) + end +end From c6c9f438d911e3b57f22e1bd3506c257db1e462b Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 14:45:42 -0400 Subject: [PATCH 4/7] test: isolate tests that require XPathVisitor subclassing --- test/css/test_xpath_visitor.rb | 64 +++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index 4e691891af1..afd3f02c979 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -65,6 +65,42 @@ def assert_xpath(expecteds, asts) end end + describe "custom pseudo-classes via XPathVisitor methods" do + it "pseudo-class functions" do + visitor = Class.new(Nokogiri::CSS::XPathVisitor) do + attr_accessor :called + + def visit_function_aaron(node) + @called = true + "aaron() = 1" + end + end.new + + assert_equal( + ["//a[aaron() = 1]"], + Nokogiri::CSS.xpath_for("a:aaron()", visitor: visitor), + ) + assert visitor.called + end + + it "pseudo-classes selectors" do + visitor = Class.new(Nokogiri::CSS::XPathVisitor) do + attr_accessor :called + + def visit_pseudo_class_aaron(node) + @called = true + "aaron() = 1" + end + end.new + + assert_equal( + ["//a[aaron() = 1]"], + Nokogiri::CSS.xpath_for("a:aaron", visitor: visitor), + ) + assert visitor.called + end + end + describe "selectors" do it "* universal" do assert_xpath("//*", parser.parse("*")) @@ -483,34 +519,6 @@ def assert_xpath(expecteds, asts) # TODO: it's unclear how this is useful and we should consider deprecating it assert_xpath("//self::div", parser.parse("self(div)")) end - - it "supports custom functions" do - visitor = Class.new(Nokogiri::CSS::XPathVisitor) do - attr_accessor :awesome - - def visit_function_aaron(node) - @awesome = true - "aaron() = 1" - end - end.new - ast = parser.parse("a:aaron()").first - assert_equal "a[aaron() = 1]", visitor.accept(ast) - assert visitor.awesome - end - - it "supports custom pseudo-classes" do - visitor = Class.new(Nokogiri::CSS::XPathVisitor) do - attr_accessor :awesome - - def visit_pseudo_class_aaron(node) - @awesome = true - "aaron() = 1" - end - end.new - ast = parser.parse("a:aaron").first - assert_equal "a[aaron() = 1]", visitor.accept(ast) - assert visitor.awesome - end end it "handles pseudo-class with class selector" do From acd9e31f8a9364401c1f9d4351cc8d010041a287 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 15:23:19 -0400 Subject: [PATCH 5/7] test: test_xpath_visitor.rb uses CSS.to_xpath for selector tests --- test/css/test_xpath_visitor.rb | 397 +++++++++++++++++---------------- 1 file changed, 199 insertions(+), 198 deletions(-) diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index afd3f02c979..5b07dfad44a 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -4,13 +4,14 @@ require "helper" describe Nokogiri::CSS::XPathVisitor do - let(:parser) { Nokogiri::CSS::Parser.new } let(:visitor) { Nokogiri::CSS::XPathVisitor.new } - def assert_xpath(expecteds, asts) - expecteds = [expecteds].flatten - expecteds.zip(asts).each do |expected, actual| - assert_equal(expected, actual.to_xpath(visitor)) + def assert_xpath(expecteds, input_selector_list) + expecteds = Array(expecteds) + actuals = Nokogiri::CSS.xpath_for(input_selector_list, visitor: visitor) + + expecteds.zip(actuals).each do |expected, actual| + assert_equal(expected, actual) end end @@ -103,71 +104,71 @@ def visit_pseudo_class_aaron(node) describe "selectors" do it "* universal" do - assert_xpath("//*", parser.parse("*")) + assert_xpath("//*", "*") end it "type" do - assert_xpath("//x", parser.parse("x")) + assert_xpath("//x", "x") end it "type with namespaces" do - assert_xpath("//aaron:a", parser.parse("aaron|a")) - assert_xpath("//a", parser.parse("|a")) + assert_xpath("//aaron:a", "aaron|a") + assert_xpath("//a", "|a") end it ". class" do assert_xpath( "//*[contains(concat(' ',normalize-space(@class),' '),' awesome ')]", - parser.parse(".awesome"), + ".awesome", ) assert_xpath( "//foo[contains(concat(' ',normalize-space(@class),' '),' awesome ')]", - parser.parse("foo.awesome"), + "foo.awesome", ) assert_xpath( "//foo//*[contains(concat(' ',normalize-space(@class),' '),' awesome ')]", - parser.parse("foo .awesome"), + "foo .awesome", ) assert_xpath( "//foo//*[contains(concat(' ',normalize-space(@class),' '),' awe.some ')]", - parser.parse("foo .awe\\.some"), + "foo .awe\\.some", ) assert_xpath( "//*[contains(concat(' ',normalize-space(@class),' '),' a ') and contains(concat(' ',normalize-space(@class),' '),' b ')]", - parser.parse(".a.b"), + ".a.b", ) assert_xpath( "//*[contains(concat(' ',normalize-space(@class),' '),' pastoral ')]", - parser.parse("*.pastoral"), + "*.pastoral", ) end it "# id" do - assert_xpath("//*[@id='foo']", parser.parse("#foo")) - assert_xpath("//*[@id='escape:needed,']", parser.parse("#escape\\:needed\\,")) - assert_xpath("//*[@id='escape:needed,']", parser.parse('#escape\3Aneeded\,')) - assert_xpath("//*[@id='escape:needed,']", parser.parse('#escape\3A needed\2C')) - assert_xpath("//*[@id='escape:needed']", parser.parse('#escape\00003Aneeded')) + assert_xpath("//*[@id='foo']", "#foo") + assert_xpath("//*[@id='escape:needed,']", "#escape\\:needed\\,") + assert_xpath("//*[@id='escape:needed,']", '#escape\3Aneeded\,') + assert_xpath("//*[@id='escape:needed,']", '#escape\3A needed\2C') + assert_xpath("//*[@id='escape:needed']", '#escape\00003Aneeded') end describe "attribute" do it "basic mechanics" do - assert_xpath("//h1[@a='Tender Lovemaking']", parser.parse("h1[a='Tender Lovemaking']")) - assert_xpath("//h1[@a]", parser.parse("h1[a]")) - assert_xpath(%q{//h1[@a='gnewline\n']}, parser.parse("h1[a='\\gnew\\\nline\\\\n']")) - assert_xpath("//h1[@a='test']", parser.parse(%q{h1[a=\te\st]})) + assert_xpath("//h1[@a='Tender Lovemaking']", "h1[a='Tender Lovemaking']") + assert_xpath("//h1[@a]", "h1[a]") + assert_xpath(%q{//h1[@a='gnewline\n']}, "h1[a='\\gnew\\\nline\\\\n']") + assert_xpath("//h1[@a='test']", %q{h1[a=\te\st]}) end it "parses leading @ (non-standard)" do - assert_xpath("//a[@id='Boing']", parser.parse("a[@id='Boing']")) - assert_xpath("//a[@id='Boing']", parser.parse("a[@id = 'Boing']")) - assert_xpath("//a[@id='Boing']//div", parser.parse("a[@id='Boing'] div")) + assert_xpath("//a[@id='Boing']", "a[@id='Boing']") + assert_xpath("//a[@id='Boing']", "a[@id = 'Boing']") + assert_xpath("//a[@id='Boing']//div", "a[@id='Boing'] div") end it "namespacing" do - assert_xpath("//a[@flavorjones:href]", parser.parse("a[flavorjones|href]")) - assert_xpath("//a[@href]", parser.parse("a[|href]")) - assert_xpath("//*[@flavorjones:href]", parser.parse("*[flavorjones|href]")) + assert_xpath("//a[@flavorjones:href]", "a[flavorjones|href]") + assert_xpath("//a[@href]", "a[|href]") + assert_xpath("//*[@flavorjones:href]", "*[flavorjones|href]") ns = { "xmlns" => "http://default.example.com/", @@ -191,362 +192,362 @@ def visit_pseudo_class_aaron(node) end it "rhs with quotes" do - assert_xpath(%q{//h1[@a="'"]}, parser.parse(%q{h1[a="'"]})) - assert_xpath(%q{//h1[@a=concat("'","")]}, parser.parse("h1[a='\\'']")) - assert_xpath(%q{//h1[@a=concat("",'"',"'","")]}, parser.parse(%q{h1[a='"\'']})) + assert_xpath(%q{//h1[@a="'"]}, %q{h1[a="'"]}) + assert_xpath(%q{//h1[@a=concat("'","")]}, "h1[a='\\'']") + assert_xpath(%q{//h1[@a=concat("",'"',"'","")]}, %q{h1[a='"\'']}) end it "rhs is number or string" do - assert_xpath("//img[@width='200']", parser.parse("img[width='200']")) - assert_xpath("//img[@width='200']", parser.parse("img[width=200]")) + assert_xpath("//img[@width='200']", "img[width='200']") + assert_xpath("//img[@width='200']", "img[width=200]") end it "bare" do - assert_xpath("//*[@a]//*[@b]", parser.parse("[a] [b]")) + assert_xpath("//*[@a]//*[@b]", "[a] [b]") end it "|=" do assert_xpath( "//a[@class='bar' or starts-with(@class,concat('bar','-'))]", - parser.parse("a[@class|='bar']"), + "a[@class|='bar']", ) assert_xpath( "//a[@class='bar' or starts-with(@class,concat('bar','-'))]", - parser.parse("a[@class |= 'bar']"), + "a[@class |= 'bar']", ) assert_xpath( "//a[@id='Boing' or starts-with(@id,concat('Boing','-'))]", - parser.parse("a[id|='Boing']"), + "a[id|='Boing']", ) end it "~=" do assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", - parser.parse("a[@class~='bar']"), + "a[@class~='bar']", ) assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", - parser.parse("a[@class ~= 'bar']"), + "a[@class ~= 'bar']", ) assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", - parser.parse("a[@class~=bar]"), + "a[@class~=bar]", ) assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", - parser.parse("a[@class~=\"bar\"]"), + "a[@class~=\"bar\"]", ) assert_xpath( "//a[contains(concat(' ',normalize-space(@data-words),' '),' bar ')]", - parser.parse("a[data-words~=\"bar\"]"), + "a[data-words~=\"bar\"]", ) end it "^=" do - assert_xpath("//a[starts-with(@id,'Boing')]", parser.parse("a[id^='Boing']")) - assert_xpath("//a[starts-with(@id,'Boing')]", parser.parse("a[id ^= 'Boing']")) + assert_xpath("//a[starts-with(@id,'Boing')]", "a[id^='Boing']") + assert_xpath("//a[starts-with(@id,'Boing')]", "a[id ^= 'Boing']") end it "$=" do assert_xpath( "//a[substring(@id,string-length(@id)-string-length('Boing')+1,string-length('Boing'))='Boing']", - parser.parse("a[id$='Boing']"), + "a[id$='Boing']", ) assert_xpath( "//a[substring(@id,string-length(@id)-string-length('Boing')+1,string-length('Boing'))='Boing']", - parser.parse("a[id $= 'Boing']"), + "a[id $= 'Boing']", ) end it "*=" do - assert_xpath("//a[contains(@id,'Boing')]", parser.parse("a[id*='Boing']")) - assert_xpath("//a[contains(@id,'Boing')]", parser.parse("a[id *= 'Boing']")) + assert_xpath("//a[contains(@id,'Boing')]", "a[id*='Boing']") + assert_xpath("//a[contains(@id,'Boing')]", "a[id *= 'Boing']") end it "!= (non-standard)" do - assert_xpath("//a[@id!='Boing']", parser.parse("a[id!='Boing']")) - assert_xpath("//a[@id!='Boing']", parser.parse("a[id != 'Boing']")) + assert_xpath("//a[@id!='Boing']", "a[id!='Boing']") + assert_xpath("//a[@id!='Boing']", "a[id != 'Boing']") end end end describe "pseudo-classes" do it ":first-of-type" do - assert_xpath("//a[position()=1]", parser.parse("a:first-of-type()")) - assert_xpath("//a[position()=1]", parser.parse("a:first-of-type")) # no parens + assert_xpath("//a[position()=1]", "a:first-of-type()") + assert_xpath("//a[position()=1]", "a:first-of-type") # no parens assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' b ')][position()=1]", - parser.parse("a.b:first-of-type"), + "a.b:first-of-type", ) # no parens end it ":nth-of-type" do - assert_xpath("//a[position()=99]", parser.parse("a:nth-of-type(99)")) + assert_xpath("//a[position()=99]", "a:nth-of-type(99)") assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' b ')][position()=99]", - parser.parse("a.b:nth-of-type(99)"), + "a.b:nth-of-type(99)", ) end it ":last-of-type" do - assert_xpath("//a[position()=last()]", parser.parse("a:last-of-type()")) - assert_xpath("//a[position()=last()]", parser.parse("a:last-of-type")) # no parens + assert_xpath("//a[position()=last()]", "a:last-of-type()") + assert_xpath("//a[position()=last()]", "a:last-of-type") # no parens assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' b ')][position()=last()]", - parser.parse("a.b:last-of-type"), + "a.b:last-of-type", ) # no parens end it ":nth-last-of-type" do - assert_xpath("//a[position()=last()]", parser.parse("a:nth-last-of-type(1)")) - assert_xpath("//a[position()=last()-98]", parser.parse("a:nth-last-of-type(99)")) + assert_xpath("//a[position()=last()]", "a:nth-last-of-type(1)") + assert_xpath("//a[position()=last()-98]", "a:nth-last-of-type(99)") assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' b ')][position()=last()-98]", - parser.parse("a.b:nth-last-of-type(99)"), + "a.b:nth-last-of-type(99)", ) end it ":nth and friends (non-standard)" do - assert_xpath("//a[position()=1]", parser.parse("a:first()")) - assert_xpath("//a[position()=1]", parser.parse("a:first")) # no parens - assert_xpath("//a[position()=99]", parser.parse("a:eq(99)")) - assert_xpath("//a[position()=99]", parser.parse("a:nth(99)")) - assert_xpath("//a[position()=last()]", parser.parse("a:last()")) - assert_xpath("//a[position()=last()]", parser.parse("a:last")) # no parens - assert_xpath("//a[node()]", parser.parse("a:parent")) + assert_xpath("//a[position()=1]", "a:first()") + assert_xpath("//a[position()=1]", "a:first") # no parens + assert_xpath("//a[position()=99]", "a:eq(99)") + assert_xpath("//a[position()=99]", "a:nth(99)") + assert_xpath("//a[position()=last()]", "a:last()") + assert_xpath("//a[position()=last()]", "a:last") # no parens + assert_xpath("//a[node()]", "a:parent") end it ":nth-child and friends" do - assert_xpath("//a[count(preceding-sibling::*)=0]", parser.parse("a:first-child")) - assert_xpath("//a[count(preceding-sibling::*)=98]", parser.parse("a:nth-child(99)")) - assert_xpath("//a[count(following-sibling::*)=0]", parser.parse("a:last-child")) - assert_xpath("//a[count(following-sibling::*)=0]", parser.parse("a:nth-last-child(1)")) - assert_xpath("//a[count(following-sibling::*)=98]", parser.parse("a:nth-last-child(99)")) + assert_xpath("//a[count(preceding-sibling::*)=0]", "a:first-child") + assert_xpath("//a[count(preceding-sibling::*)=98]", "a:nth-child(99)") + assert_xpath("//a[count(following-sibling::*)=0]", "a:last-child") + assert_xpath("//a[count(following-sibling::*)=0]", "a:nth-last-child(1)") + assert_xpath("//a[count(following-sibling::*)=98]", "a:nth-last-child(99)") end it "[n] as :nth-child (non-standard)" do - assert_xpath("//a[count(preceding-sibling::*)=1]", parser.parse("a[2]")) + assert_xpath("//a[count(preceding-sibling::*)=1]", "a[2]") end it ":has()" do - assert_xpath("//a[.//b]", parser.parse("a:has(b)")) - assert_xpath("//a[.//b/c]", parser.parse("a:has(b > c)")) - assert_xpath("//a[./b]", parser.parse("a:has(> b)")) - assert_xpath("//a[./following-sibling::b]", parser.parse("a:has(~ b)")) - assert_xpath("//a[./following-sibling::*[1]/self::b]", parser.parse("a:has(+ b)")) + assert_xpath("//a[.//b]", "a:has(b)") + assert_xpath("//a[.//b/c]", "a:has(b > c)") + assert_xpath("//a[./b]", "a:has(> b)") + assert_xpath("//a[./following-sibling::b]", "a:has(~ b)") + assert_xpath("//a[./following-sibling::*[1]/self::b]", "a:has(+ b)") end it ":only-child" do assert_xpath( "//a[count(preceding-sibling::*)=0 and count(following-sibling::*)=0]", - parser.parse("a:only-child"), + "a:only-child", ) end it ":only-of-type" do - assert_xpath("//a[last()=1]", parser.parse("a:only-of-type")) + assert_xpath("//a[last()=1]", "a:only-of-type") end it ":empty" do - assert_xpath("//a[not(node())]", parser.parse("a:empty")) + assert_xpath("//a[not(node())]", "a:empty") end it ":nth(an+b)" do - assert_xpath("//a[(position() mod 2)=0]", parser.parse("a:nth-of-type(2n)")) - assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", parser.parse("a:nth-of-type(2n+1)")) - assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", parser.parse("a:nth-of-type(2n + 1)")) - assert_xpath("//a[(position() mod 2)=0]", parser.parse("a:nth-of-type(even)")) - assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", parser.parse("a:nth-of-type(odd)")) - assert_xpath("//a[(position()>=3) and (((position()-3) mod 4)=0)]", parser.parse("a:nth-of-type(4n+3)")) - assert_xpath("//a[position()<=3]", parser.parse("a:nth-of-type(-1n+3)")) - assert_xpath("//a[position()<=3]", parser.parse("a:nth-of-type(-1n + 3)")) - assert_xpath("//a[position()<=3]", parser.parse("a:nth-of-type(-n+3)")) - assert_xpath("//a[position()>=3]", parser.parse("a:nth-of-type(1n+3)")) - assert_xpath("//a[position()>=3]", parser.parse("a:nth-of-type(n+3)")) - - assert_xpath("//a[((last()-position()+1) mod 2)=0]", parser.parse("a:nth-last-of-type(2n)")) - assert_xpath("//a[((last()-position()+1)>=1) and ((((last()-position()+1)-1) mod 2)=0)]", parser.parse("a:nth-last-of-type(2n+1)")) - assert_xpath("//a[((last()-position()+1) mod 2)=0]", parser.parse("a:nth-last-of-type(even)")) - assert_xpath("//a[((last()-position()+1)>=1) and ((((last()-position()+1)-1) mod 2)=0)]", parser.parse("a:nth-last-of-type(odd)")) - assert_xpath("//a[((last()-position()+1)>=3) and ((((last()-position()+1)-3) mod 4)=0)]", parser.parse("a:nth-last-of-type(4n+3)")) - assert_xpath("//a[(last()-position()+1)<=3]", parser.parse("a:nth-last-of-type(-1n+3)")) - assert_xpath("//a[(last()-position()+1)<=3]", parser.parse("a:nth-last-of-type(-n+3)")) - assert_xpath("//a[(last()-position()+1)>=3]", parser.parse("a:nth-last-of-type(1n+3)")) - assert_xpath("//a[(last()-position()+1)>=3]", parser.parse("a:nth-last-of-type(n+3)")) + assert_xpath("//a[(position() mod 2)=0]", "a:nth-of-type(2n)") + assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", "a:nth-of-type(2n+1)") + assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", "a:nth-of-type(2n + 1)") + assert_xpath("//a[(position() mod 2)=0]", "a:nth-of-type(even)") + assert_xpath("//a[(position()>=1) and (((position()-1) mod 2)=0)]", "a:nth-of-type(odd)") + assert_xpath("//a[(position()>=3) and (((position()-3) mod 4)=0)]", "a:nth-of-type(4n+3)") + assert_xpath("//a[position()<=3]", "a:nth-of-type(-1n+3)") + assert_xpath("//a[position()<=3]", "a:nth-of-type(-1n + 3)") + assert_xpath("//a[position()<=3]", "a:nth-of-type(-n+3)") + assert_xpath("//a[position()>=3]", "a:nth-of-type(1n+3)") + assert_xpath("//a[position()>=3]", "a:nth-of-type(n+3)") + + assert_xpath("//a[((last()-position()+1) mod 2)=0]", "a:nth-last-of-type(2n)") + assert_xpath("//a[((last()-position()+1)>=1) and ((((last()-position()+1)-1) mod 2)=0)]", "a:nth-last-of-type(2n+1)") + assert_xpath("//a[((last()-position()+1) mod 2)=0]", "a:nth-last-of-type(even)") + assert_xpath("//a[((last()-position()+1)>=1) and ((((last()-position()+1)-1) mod 2)=0)]", "a:nth-last-of-type(odd)") + assert_xpath("//a[((last()-position()+1)>=3) and ((((last()-position()+1)-3) mod 4)=0)]", "a:nth-last-of-type(4n+3)") + assert_xpath("//a[(last()-position()+1)<=3]", "a:nth-last-of-type(-1n+3)") + assert_xpath("//a[(last()-position()+1)<=3]", "a:nth-last-of-type(-n+3)") + assert_xpath("//a[(last()-position()+1)>=3]", "a:nth-last-of-type(1n+3)") + assert_xpath("//a[(last()-position()+1)>=3]", "a:nth-last-of-type(n+3)") end it ":not()" do - assert_xpath("//ol/*[not(self::li)]", parser.parse("ol > *:not(li)")) + assert_xpath("//ol/*[not(self::li)]", "ol > *:not(li)") assert_xpath( "//*[@id='p' and not(contains(concat(' ',normalize-space(@class),' '),' a '))]", - parser.parse("#p:not(.a)"), + "#p:not(.a)", ) assert_xpath( "//p[contains(concat(' ',normalize-space(@class),' '),' a ') and not(contains(concat(' ',normalize-space(@class),' '),' b '))]", - parser.parse("p.a:not(.b)"), + "p.a:not(.b)", ) assert_xpath( "//p[@a='foo' and not(contains(concat(' ',normalize-space(@class),' '),' b '))]", - parser.parse("p[a='foo']:not(.b)"), + "p[a='foo']:not(.b)", ) end it "chained :not()" do assert_xpath( "//p[not(contains(concat(' ',normalize-space(@class),' '),' a ')) and not(contains(concat(' ',normalize-space(@class),' '),' b ')) and not(contains(concat(' ',normalize-space(@class),' '),' c '))]", - parser.parse("p:not(.a):not(.b):not(.c)"), + "p:not(.a):not(.b):not(.c)", ) end it "combinations of :not() and nth-and-friends" do assert_xpath( "//ol/*[not(count(following-sibling::*)=0)]", - parser.parse("ol > *:not(:last-child)"), + "ol > *:not(:last-child)", ) assert_xpath( "//ol/*[not(count(preceding-sibling::*)=0 and count(following-sibling::*)=0)]", - parser.parse("ol > *:not(:only-child)"), + "ol > *:not(:only-child)", ) end it "miscellaneous pseudo-classes are converted into xpath function calls" do - assert_xpath("//a[nokogiri:aaron(.)]", parser.parse("a:aaron")) - assert_xpath("//a[nokogiri:aaron(.)]", parser.parse("a:aaron()")) - assert_xpath("//a[nokogiri:aaron(.,12)]", parser.parse("a:aaron(12)")) - assert_xpath("//a[nokogiri:aaron(.,12,1)]", parser.parse("a:aaron(12, 1)")) + assert_xpath("//a[nokogiri:aaron(.)]", "a:aaron") + assert_xpath("//a[nokogiri:aaron(.)]", "a:aaron()") + assert_xpath("//a[nokogiri:aaron(.,12)]", "a:aaron(12)") + assert_xpath("//a[nokogiri:aaron(.,12,1)]", "a:aaron(12, 1)") - assert_xpath("//a[nokogiri:link(.)]", parser.parse("a:link")) - assert_xpath("//a[nokogiri:visited(.)]", parser.parse("a:visited")) - assert_xpath("//a[nokogiri:hover(.)]", parser.parse("a:hover")) - assert_xpath("//a[nokogiri:active(.)]", parser.parse("a:active")) + assert_xpath("//a[nokogiri:link(.)]", "a:link") + assert_xpath("//a[nokogiri:visited(.)]", "a:visited") + assert_xpath("//a[nokogiri:hover(.)]", "a:hover") + assert_xpath("//a[nokogiri:active(.)]", "a:active") - assert_xpath("//a[nokogiri:foo(.,@href)]", parser.parse("a:foo(@href)")) - assert_xpath("//a[nokogiri:foo(.,@href,@id)]", parser.parse("a:foo(@href, @id)")) - assert_xpath("//a[nokogiri:foo(.,@a,b)]", parser.parse("a:foo(@a, b)")) - assert_xpath("//a[nokogiri:foo(.,a,@b)]", parser.parse("a:foo(a, @b)")) - assert_xpath("//a[nokogiri:foo(.,a,10)]", parser.parse("a:foo(a, 10)")) - assert_xpath("//a[nokogiri:foo(.,42)]", parser.parse("a:foo(42)")) - assert_xpath("//a[nokogiri:foo(.,'bar')]", parser.parse("a:foo('bar')")) + assert_xpath("//a[nokogiri:foo(.,@href)]", "a:foo(@href)") + assert_xpath("//a[nokogiri:foo(.,@href,@id)]", "a:foo(@href, @id)") + assert_xpath("//a[nokogiri:foo(.,@a,b)]", "a:foo(@a, b)") + assert_xpath("//a[nokogiri:foo(.,a,@b)]", "a:foo(a, @b)") + assert_xpath("//a[nokogiri:foo(.,a,10)]", "a:foo(a, 10)") + assert_xpath("//a[nokogiri:foo(.,42)]", "a:foo(42)") + assert_xpath("//a[nokogiri:foo(.,'bar')]", "a:foo('bar')") end it "bare pseudo-class matches any ident" do - assert_xpath("//*[nokogiri:link(.)]", parser.parse(":link")) - assert_xpath("//*[not(@id='foo')]", parser.parse(":not(#foo)")) - assert_xpath("//*[count(preceding-sibling::*)=0]", parser.parse(":first-child")) + assert_xpath("//*[nokogiri:link(.)]", ":link") + assert_xpath("//*[not(@id='foo')]", ":not(#foo)") + assert_xpath("//*[count(preceding-sibling::*)=0]", ":first-child") end end describe "combinators" do it "descendant" do - assert_xpath("//x//y", parser.parse("x y")) + assert_xpath("//x//y", "x y") end it "~ general sibling" do - assert_xpath("//E/following-sibling::F", parser.parse("E ~ F")) - assert_xpath("//E/following-sibling::F//G", parser.parse("E ~ F G")) + assert_xpath("//E/following-sibling::F", "E ~ F") + assert_xpath("//E/following-sibling::F//G", "E ~ F G") end it "~ general sibling prefixless is relative to context node" do - assert_xpath("./following-sibling::a", parser.parse("~a")) - assert_xpath("./following-sibling::a", parser.parse("~ a")) - assert_xpath("./following-sibling::a//b/following-sibling::i", parser.parse("~a b~i")) - assert_xpath("./following-sibling::a//b/following-sibling::i", parser.parse("~ a b ~ i")) + assert_xpath("./following-sibling::a", "~a") + assert_xpath("./following-sibling::a", "~ a") + assert_xpath("./following-sibling::a//b/following-sibling::i", "~a b~i") + assert_xpath("./following-sibling::a//b/following-sibling::i", "~ a b ~ i") end it "+ adjacent sibling" do - assert_xpath("//E/following-sibling::*[1]/self::F", parser.parse("E + F")) - assert_xpath("//E/following-sibling::*[1]/self::F//G", parser.parse("E + F G")) + assert_xpath("//E/following-sibling::*[1]/self::F", "E + F") + assert_xpath("//E/following-sibling::*[1]/self::F//G", "E + F G") end it "+ adjacent sibling prefixless is relative to context node" do - assert_xpath("./following-sibling::*[1]/self::a", parser.parse("+a")) - assert_xpath("./following-sibling::*[1]/self::a", parser.parse("+ a")) - assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", parser.parse("+a+b")) - assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", parser.parse("+ a + b")) + assert_xpath("./following-sibling::*[1]/self::a", "+a") + assert_xpath("./following-sibling::*[1]/self::a", "+ a") + assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", "+a+b") + assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", "+ a + b") end it "> child" do - assert_xpath("//x/y", parser.parse("x > y")) - assert_xpath("//a//b/i", parser.parse("a b>i")) - assert_xpath("//a//b/i", parser.parse("a b > i")) - assert_xpath("//a/b/i", parser.parse("a > b > i")) + assert_xpath("//x/y", "x > y") + assert_xpath("//a//b/i", "a b>i") + assert_xpath("//a//b/i", "a b > i") + assert_xpath("//a/b/i", "a > b > i") end it "> child prefixless is relative to context node" do - assert_xpath("./a", parser.parse(">a")) - assert_xpath("./a", parser.parse("> a")) - assert_xpath("./a//b/i", parser.parse(">a b>i")) - assert_xpath("./a/b/i", parser.parse("> a > b > i")) + assert_xpath("./a", ">a") + assert_xpath("./a", "> a") + assert_xpath("./a//b/i", ">a b>i") + assert_xpath("./a/b/i", "> a > b > i") end it "/ (non-standard)" do - assert_xpath("//x/y", parser.parse("x/y")) - assert_xpath("//x/y", parser.parse("x / y")) + assert_xpath("//x/y", "x/y") + assert_xpath("//x/y", "x / y") end it "// (non-standard)" do - assert_xpath("//x//y", parser.parse("x//y")) - assert_xpath("//x//y", parser.parse("x // y")) + assert_xpath("//x//y", "x//y") + assert_xpath("//x//y", "x // y") end end describe "functions" do it "handles text() (non-standard)" do - assert_xpath("//a[child::text()]", parser.parse("a[text()]")) - assert_xpath("//child::text()", parser.parse("text()")) - assert_xpath("//a//child::text()", parser.parse("a text()")) - assert_xpath("//a/child::text()", parser.parse("a / text()")) - assert_xpath("//a/child::text()", parser.parse("a > text()")) - assert_xpath("//a//child::text()", parser.parse("a text()")) + assert_xpath("//a[child::text()]", "a[text()]") + assert_xpath("//child::text()", "text()") + assert_xpath("//a//child::text()", "a text()") + assert_xpath("//a/child::text()", "a / text()") + assert_xpath("//a/child::text()", "a > text()") + assert_xpath("//a//child::text()", "a text()") end it "handles comment() (non-standard)" do - assert_xpath("//script//comment()", parser.parse("script comment()")) + assert_xpath("//script//comment()", "script comment()") end it "handles contains() (non-standard)" do # https://api.jquery.com/contains-selector/ - assert_xpath(%{//div[contains(.,"youtube")]}, parser.parse(%{div:contains("youtube")})) + assert_xpath(%{//div[contains(.,"youtube")]}, %{div:contains("youtube")}) end it "handles gt() (non-standard)" do # https://api.jquery.com/gt-selector/ - assert_xpath("//td[position()>3]", parser.parse("td:gt(3)")) + assert_xpath("//td[position()>3]", "td:gt(3)") end it "handles self()" do # TODO: it's unclear how this is useful and we should consider deprecating it - assert_xpath("//self::div", parser.parse("self(div)")) + assert_xpath("//self::div", "self(div)") end end it "handles pseudo-class with class selector" do assert_xpath( "//a[nokogiri:active(.) and contains(concat(' ',normalize-space(@class),' '),' foo ')]", - parser.parse("a:active.foo"), + "a:active.foo", ) assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' foo ') and nokogiri:active(.)]", - parser.parse("a.foo:active"), + "a.foo:active", ) end it "handles pseudo-class with an id selector" do - assert_xpath("//a[@id='foo' and nokogiri:active(.)]", parser.parse("a#foo:active")) - assert_xpath("//a[nokogiri:active(.) and @id='foo']", parser.parse("a:active#foo")) + assert_xpath("//a[@id='foo' and nokogiri:active(.)]", "a#foo:active") + assert_xpath("//a[nokogiri:active(.) and @id='foo']", "a:active#foo") end it "handles function with pseudo-class" do - assert_xpath("//child::text()[position()=99]", parser.parse("text():nth-of-type(99)")) + assert_xpath("//child::text()[position()=99]", "text():nth-of-type(99)") end it "handles multiple selectors" do - assert_xpath(["//x/y", "//y/z"], parser.parse("x > y, y > z")) - assert_xpath(["//x/y", "//y/z"], parser.parse("x > y,y > z")) + assert_xpath(["//x/y", "//y/z"], "x > y, y > z") + assert_xpath(["//x/y", "//y/z"], "x > y,y > z") ### # TODO: should we make this work? - # assert_xpath ['//x/y', '//y/z'], parser.parse('x > y | y > z') + # assert_xpath ['//x/y', '//y/z'], 'x > y | y > z' end describe "builtins:always" do @@ -565,50 +566,50 @@ def visit_pseudo_class_aaron(node) it ". class" do assert_xpath( "//*[nokogiri-builtin:css-class(@class,'awesome')]", - parser.parse(".awesome"), + ".awesome", ) assert_xpath( "//foo[nokogiri-builtin:css-class(@class,'awesome')]", - parser.parse("foo.awesome"), + "foo.awesome", ) assert_xpath( "//foo//*[nokogiri-builtin:css-class(@class,'awesome')]", - parser.parse("foo .awesome"), + "foo .awesome", ) assert_xpath( "//foo//*[nokogiri-builtin:css-class(@class,'awe.some')]", - parser.parse("foo .awe\\.some"), + "foo .awe\\.some", ) assert_xpath( "//*[nokogiri-builtin:css-class(@class,'a') and nokogiri-builtin:css-class(@class,'b')]", - parser.parse(".a.b"), + ".a.b", ) end it "~=" do assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - parser.parse("a[@class~='bar']"), + "a[@class~='bar']", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - parser.parse("a[@class ~= 'bar']"), + "a[@class ~= 'bar']", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - parser.parse("a[@class~=bar]"), + "a[@class~=bar]", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - parser.parse("a[@class~=\"bar\"]"), + "a[@class~=\"bar\"]", ) assert_xpath( "//a[nokogiri-builtin:css-class(@data-words,'bar')]", - parser.parse("a[data-words~=\"bar\"]"), + "a[data-words~=\"bar\"]", ) assert_xpath( "//a[nokogiri-builtin:css-class(@data-words,'bar')]", - parser.parse("a[@data-words~=\"bar\"]"), + "a[@data-words~=\"bar\"]", ) end end @@ -670,12 +671,12 @@ def visit_pseudo_class_aaron(node) if Nokogiri.uses_libxml? assert_xpath( "//*[nokogiri-builtin:css-class(@class,'awesome')]", - parser.parse(".awesome"), + ".awesome", ) else assert_xpath( "//*[contains(concat(' ',normalize-space(@class),' '),' awesome ')]", - parser.parse(".awesome"), + ".awesome", ) end end @@ -684,12 +685,12 @@ def visit_pseudo_class_aaron(node) if Nokogiri.uses_libxml? assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - parser.parse("a[@class~='bar']"), + "a[@class~='bar']", ) else assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", - parser.parse("a[@class~='bar']"), + "a[@class~='bar']", ) end end @@ -736,21 +737,21 @@ def visit_pseudo_class_aaron(node) it "matches on the element's local-name, ignoring namespaces" do if Nokogiri.libxml2_patches.include?("0009-allow-wildcard-namespaces.patch") - assert_xpath("//*:foo", parser.parse("foo")) + assert_xpath("//*:foo", "foo") else - assert_xpath("//*[nokogiri-builtin:local-name-is('foo')]", parser.parse("foo")) + assert_xpath("//*[nokogiri-builtin:local-name-is('foo')]", "foo") end end it "avoids the wildcard when using namespaces" do - assert_xpath("//ns1:foo", parser.parse("ns1|foo")) + assert_xpath("//ns1:foo", "ns1|foo") end it "avoids the wildcard when using attribute selectors" do if Nokogiri.libxml2_patches.include?("0009-allow-wildcard-namespaces.patch") - assert_xpath("//*:a/@href", parser.parse("a/@href")) + assert_xpath("//*:a/@href", "a/@href") else - assert_xpath("//*[nokogiri-builtin:local-name-is('a')]/@href", parser.parse("a/@href")) + assert_xpath("//*[nokogiri-builtin:local-name-is('a')]/@href", "a/@href") end end end @@ -758,11 +759,11 @@ def visit_pseudo_class_aaron(node) describe "builtins:never" do let(:builtins) { Nokogiri::CSS::XPathVisitor::BuiltinsConfig::NEVER } it "matches on the element's local-name, ignoring namespaces" do - assert_xpath("//*[local-name()='foo']", parser.parse("foo")) + assert_xpath("//*[local-name()='foo']", "foo") end it "avoids the wildcard when using attribute selectors" do - assert_xpath("//*[local-name()='a']/@href", parser.parse("a/@href")) + assert_xpath("//*[local-name()='a']/@href", "a/@href") end end @@ -771,12 +772,12 @@ def visit_pseudo_class_aaron(node) it "matches on the element's local-name, ignoring namespaces" do if Nokogiri.uses_libxml? if Nokogiri.libxml2_patches.include?("0009-allow-wildcard-namespaces.patch") - assert_xpath("//*:foo", parser.parse("foo")) + assert_xpath("//*:foo", "foo") else - assert_xpath("//*[nokogiri-builtin:local-name-is('foo')]", parser.parse("foo")) + assert_xpath("//*[nokogiri-builtin:local-name-is('foo')]", "foo") end else - assert_xpath("//*[local-name()='foo']", parser.parse("foo")) + assert_xpath("//*[local-name()='foo']", "foo") end end end From dc587c5171feed788fbeb8e8dff7d8d8ef85b2c6 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 16:12:56 -0400 Subject: [PATCH 6/7] test: clearly mark extended CSS syntax tests as such using "extended-syntax" in the test name --- test/css/test_xpath_visitor.rb | 110 ++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 21 deletions(-) diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index 5b07dfad44a..dc2b892121f 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -159,7 +159,7 @@ def visit_pseudo_class_aaron(node) assert_xpath("//h1[@a='test']", %q{h1[a=\te\st]}) end - it "parses leading @ (non-standard)" do + it "parses leading @ (extended-syntax)" do assert_xpath("//a[@id='Boing']", "a[@id='Boing']") assert_xpath("//a[@id='Boing']", "a[@id = 'Boing']") assert_xpath("//a[@id='Boing']//div", "a[@id='Boing'] div") @@ -207,6 +207,21 @@ def visit_pseudo_class_aaron(node) end it "|=" do + assert_xpath( + "//a[@class='bar' or starts-with(@class,concat('bar','-'))]", + "a[class|='bar']", + ) + assert_xpath( + "//a[@class='bar' or starts-with(@class,concat('bar','-'))]", + "a[class |= 'bar']", + ) + assert_xpath( + "//a[@id='Boing' or starts-with(@id,concat('Boing','-'))]", + "a[id|='Boing']", + ) + end + + it "|= (extended-syntax)" do assert_xpath( "//a[@class='bar' or starts-with(@class,concat('bar','-'))]", "a[@class|='bar']", @@ -217,11 +232,34 @@ def visit_pseudo_class_aaron(node) ) assert_xpath( "//a[@id='Boing' or starts-with(@id,concat('Boing','-'))]", - "a[id|='Boing']", + "a[@id|='Boing']", ) end it "~=" do + assert_xpath( + "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", + "a[class~='bar']", + ) + assert_xpath( + "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", + "a[class ~= 'bar']", + ) + assert_xpath( + "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", + "a[class~=bar]", + ) + assert_xpath( + "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", + "a[class~=\"bar\"]", + ) + assert_xpath( + "//a[contains(concat(' ',normalize-space(@data-words),' '),' bar ')]", + "a[data-words~=\"bar\"]", + ) + end + + it "~= (extended-syntax)" do assert_xpath( "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", "a[@class~='bar']", @@ -240,7 +278,7 @@ def visit_pseudo_class_aaron(node) ) assert_xpath( "//a[contains(concat(' ',normalize-space(@data-words),' '),' bar ')]", - "a[data-words~=\"bar\"]", + "a[@data-words~=\"bar\"]", ) end @@ -265,7 +303,7 @@ def visit_pseudo_class_aaron(node) assert_xpath("//a[contains(@id,'Boing')]", "a[id *= 'Boing']") end - it "!= (non-standard)" do + it "!= (extended-syntax)" do assert_xpath("//a[@id!='Boing']", "a[id!='Boing']") assert_xpath("//a[@id!='Boing']", "a[id != 'Boing']") end @@ -308,7 +346,7 @@ def visit_pseudo_class_aaron(node) ) end - it ":nth and friends (non-standard)" do + it ":nth and friends (extended-syntax)" do assert_xpath("//a[position()=1]", "a:first()") assert_xpath("//a[position()=1]", "a:first") # no parens assert_xpath("//a[position()=99]", "a:eq(99)") @@ -326,7 +364,7 @@ def visit_pseudo_class_aaron(node) assert_xpath("//a[count(following-sibling::*)=98]", "a:nth-last-child(99)") end - it "[n] as :nth-child (non-standard)" do + it "[n] as :nth-child (extended-syntax)" do assert_xpath("//a[count(preceding-sibling::*)=1]", "a[2]") end @@ -481,19 +519,19 @@ def visit_pseudo_class_aaron(node) assert_xpath("./a/b/i", "> a > b > i") end - it "/ (non-standard)" do + it "/ (extended-syntax)" do assert_xpath("//x/y", "x/y") assert_xpath("//x/y", "x / y") end - it "// (non-standard)" do + it "// (extended-syntax)" do assert_xpath("//x//y", "x//y") assert_xpath("//x//y", "x // y") end end describe "functions" do - it "handles text() (non-standard)" do + it "handles text() (extended-syntax)" do assert_xpath("//a[child::text()]", "a[text()]") assert_xpath("//child::text()", "text()") assert_xpath("//a//child::text()", "a text()") @@ -502,16 +540,16 @@ def visit_pseudo_class_aaron(node) assert_xpath("//a//child::text()", "a text()") end - it "handles comment() (non-standard)" do + it "handles comment() (extended-syntax)" do assert_xpath("//script//comment()", "script comment()") end - it "handles contains() (non-standard)" do + it "handles contains() (extended-syntax)" do # https://api.jquery.com/contains-selector/ assert_xpath(%{//div[contains(.,"youtube")]}, %{div:contains("youtube")}) end - it "handles gt() (non-standard)" do + it "handles gt() (extended-syntax)" do # https://api.jquery.com/gt-selector/ assert_xpath("//td[position()>3]", "td:gt(3)") end @@ -545,9 +583,6 @@ def visit_pseudo_class_aaron(node) it "handles multiple selectors" do assert_xpath(["//x/y", "//y/z"], "x > y, y > z") assert_xpath(["//x/y", "//y/z"], "x > y,y > z") - ### - # TODO: should we make this work? - # assert_xpath ['//x/y', '//y/z'], 'x > y | y > z' end describe "builtins:always" do @@ -589,24 +624,43 @@ def visit_pseudo_class_aaron(node) it "~=" do assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - "a[@class~='bar']", + "a[class~='bar']", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - "a[@class ~= 'bar']", + "a[class ~= 'bar']", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - "a[@class~=bar]", + "a[class~=bar]", ) assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", - "a[@class~=\"bar\"]", + "a[class~=\"bar\"]", ) assert_xpath( "//a[nokogiri-builtin:css-class(@data-words,'bar')]", "a[data-words~=\"bar\"]", ) + end + + it "~= (extended-syntax)" do + assert_xpath( + "//a[nokogiri-builtin:css-class(@class,'bar')]", + "a[@class~='bar']", + ) + assert_xpath( + "//a[nokogiri-builtin:css-class(@class,'bar')]", + "a[@class ~= 'bar']", + ) + assert_xpath( + "//a[nokogiri-builtin:css-class(@class,'bar')]", + "a[@class~=bar]", + ) + assert_xpath( + "//a[nokogiri-builtin:css-class(@class,'bar')]", + "a[@class~=\"bar\"]", + ) assert_xpath( "//a[nokogiri-builtin:css-class(@data-words,'bar')]", "a[@data-words~=\"bar\"]", @@ -682,6 +736,20 @@ def visit_pseudo_class_aaron(node) end it "~=" do + if Nokogiri.uses_libxml? + assert_xpath( + "//a[nokogiri-builtin:css-class(@class,'bar')]", + "a[class~='bar']", + ) + else + assert_xpath( + "//a[contains(concat(' ',normalize-space(@class),' '),' bar ')]", + "a[class~='bar']", + ) + end + end + + it "~= (extended-syntax)" do if Nokogiri.uses_libxml? assert_xpath( "//a[nokogiri-builtin:css-class(@class,'bar')]", @@ -747,7 +815,7 @@ def visit_pseudo_class_aaron(node) assert_xpath("//ns1:foo", "ns1|foo") end - it "avoids the wildcard when using attribute selectors" do + it "avoids the wildcard when using attribute selectors (extended-syntax)" do if Nokogiri.libxml2_patches.include?("0009-allow-wildcard-namespaces.patch") assert_xpath("//*:a/@href", "a/@href") else @@ -762,7 +830,7 @@ def visit_pseudo_class_aaron(node) assert_xpath("//*[local-name()='foo']", "foo") end - it "avoids the wildcard when using attribute selectors" do + it "avoids the wildcard when using attribute selectors (extended-syntax)" do assert_xpath("//*[local-name()='a']/@href", "a/@href") end end From 0f367e61a8c1a3af3788aa2b593483b85af4a215 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 11 Jun 2024 16:33:36 -0400 Subject: [PATCH 7/7] test: make sure we set cache: false in xpath visitor tests --- test/css/test_xpath_visitor.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/css/test_xpath_visitor.rb b/test/css/test_xpath_visitor.rb index dc2b892121f..56a544b8528 100644 --- a/test/css/test_xpath_visitor.rb +++ b/test/css/test_xpath_visitor.rb @@ -8,7 +8,7 @@ def assert_xpath(expecteds, input_selector_list) expecteds = Array(expecteds) - actuals = Nokogiri::CSS.xpath_for(input_selector_list, visitor: visitor) + actuals = Nokogiri::CSS.xpath_for(input_selector_list, visitor: visitor, cache: false) expecteds.zip(actuals).each do |expected, actual| assert_equal(expected, actual) @@ -79,7 +79,7 @@ def visit_function_aaron(node) assert_equal( ["//a[aaron() = 1]"], - Nokogiri::CSS.xpath_for("a:aaron()", visitor: visitor), + Nokogiri::CSS.xpath_for("a:aaron()", visitor: visitor, cache: false), ) assert visitor.called end @@ -96,7 +96,7 @@ def visit_pseudo_class_aaron(node) assert_equal( ["//a[aaron() = 1]"], - Nokogiri::CSS.xpath_for("a:aaron", visitor: visitor), + Nokogiri::CSS.xpath_for("a:aaron", visitor: visitor, cache: false), ) assert visitor.called end @@ -176,18 +176,18 @@ def visit_pseudo_class_aaron(node) } # An intentionally-empty namespace means "don't use the default xmlns" - assert_equal(["//a"], Nokogiri::CSS.xpath_for("|a", ns: ns)) + assert_equal(["//a"], Nokogiri::CSS.xpath_for("|a", ns: ns, cache: false)) # The default namespace is not applied to attributes (just elements) assert_equal( ["//xmlns:a[@class='bar']"], - Nokogiri::CSS.xpath_for("a[class='bar']", ns: ns), + Nokogiri::CSS.xpath_for("a[class='bar']", ns: ns, cache: false), ) # We can explicitly apply a namespace to an attribue assert_equal( ["//xmlns:a[@hoge:class='bar']"], - Nokogiri::CSS.xpath_for("a[hoge|class='bar']", ns: ns), + Nokogiri::CSS.xpath_for("a[hoge|class='bar']", ns: ns, cache: false), ) end