blob: bd11f1cd76f3e03bb26794bf562955c38039b375 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/css/selector_query.h"
#include <memory>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/css/parser/css_parser.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_context.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/static_node_list.h"
#include "third_party/blink/renderer/core/html/html_document.h"
#include "third_party/blink/renderer/core/html/html_html_element.h"
// Uncomment to run the SelectorQueryTests for stats in a release build.
// #define RELEASE_QUERY_STATS
namespace blink {
namespace {
struct QueryTest {
const char* selector;
bool query_all;
unsigned matches;
// {totalCount, fastId, fastClass, fastTagName, fastScan, slowScan,
// slowTraversingShadowTreeScan}
SelectorQuery::QueryStats stats;
};
template <unsigned length>
void RunTests(ContainerNode& scope, const QueryTest (&test_cases)[length]) {
for (const auto& test_case : test_cases) {
const char* selector = test_case.selector;
SCOPED_TRACE(testing::Message()
<< (test_case.query_all ? "querySelectorAll('"
: "querySelector('")
<< selector << "')");
if (test_case.query_all) {
StaticElementList* match_all = scope.QuerySelectorAll(selector);
EXPECT_EQ(test_case.matches, match_all->length());
} else {
Element* match = scope.QuerySelector(selector);
EXPECT_EQ(test_case.matches, match ? 1u : 0u);
}
#if DCHECK_IS_ON() || defined(RELEASE_QUERY_STATS)
SelectorQuery::QueryStats stats = SelectorQuery::LastQueryStats();
EXPECT_EQ(test_case.stats.total_count, stats.total_count);
EXPECT_EQ(test_case.stats.fast_id, stats.fast_id);
EXPECT_EQ(test_case.stats.fast_class, stats.fast_class);
EXPECT_EQ(test_case.stats.fast_tag_name, stats.fast_tag_name);
EXPECT_EQ(test_case.stats.fast_scan, stats.fast_scan);
EXPECT_EQ(test_case.stats.slow_scan, stats.slow_scan);
EXPECT_EQ(test_case.stats.slow_traversing_shadow_tree_scan,
stats.slow_traversing_shadow_tree_scan);
#endif
}
}
} // namespace
TEST(SelectorQueryTest, NotMatchingPseudoElement) {
Document* document = Document::CreateForTest();
HTMLHtmlElement* html = HTMLHtmlElement::Create(*document);
document->AppendChild(html);
document->documentElement()->SetInnerHTMLFromString(
"<body><style>span::before { content: 'X' }</style><span></span></body>");
CSSSelectorList selector_list = CSSParser::ParseSelector(
CSSParserContext::Create(*document, NullURL(), true /* origin_clean */,
network::mojom::ReferrerPolicy::kDefault,
WTF::TextEncoding(),
CSSParserContext::kSnapshotProfile),
nullptr, "span::before");
std::unique_ptr<SelectorQuery> query =
SelectorQuery::Adopt(std::move(selector_list));
Element* elm = query->QueryFirst(*document);
EXPECT_EQ(nullptr, elm);
selector_list = CSSParser::ParseSelector(
CSSParserContext::Create(*document, NullURL(), true /* origin_clean */,
network::mojom::ReferrerPolicy::kDefault,
WTF::TextEncoding(),
CSSParserContext::kSnapshotProfile),
nullptr, "span");
query = SelectorQuery::Adopt(std::move(selector_list));
elm = query->QueryFirst(*document);
EXPECT_NE(nullptr, elm);
}
TEST(SelectorQueryTest, LastOfTypeNotFinishedParsing) {
Document* document = HTMLDocument::CreateForTest();
HTMLHtmlElement* html = HTMLHtmlElement::Create(*document);
document->AppendChild(html);
document->documentElement()->SetInnerHTMLFromString(
"<body><p></p><p id=last></p></body>", ASSERT_NO_EXCEPTION);
document->body()->BeginParsingChildren();
CSSSelectorList selector_list = CSSParser::ParseSelector(
CSSParserContext::Create(*document, NullURL(), true /* origin_clean */,
network::mojom::ReferrerPolicy::kDefault,
WTF::TextEncoding(),
CSSParserContext::kSnapshotProfile),
nullptr, "p:last-of-type");
std::unique_ptr<SelectorQuery> query =
SelectorQuery::Adopt(std::move(selector_list));
Element* elm = query->QueryFirst(*document);
ASSERT_TRUE(elm);
EXPECT_EQ("last", elm->IdForStyleResolution());
}
TEST(SelectorQueryTest, StandardsModeFastPaths) {
Document* document = HTMLDocument::CreateForTest();
document->write(R"HTML(
<!DOCTYPE html>
<html>
<head></head>
<body>
<span id=first class=A>
<span id=a class=one></span>
<span id=b class=two></span>
<span id=c class=one></span>
<div id=multiple class=two></div>
</span>
<div>
<span id=second class=B>
<span id=A class=one></span>
<span id=B class=two></span>
<span id=C class=one></span>
<span id=multiple class=two></span>
</span>
</div>
</body>
</html>
)HTML");
static const struct QueryTest kTestCases[] = {
// Id in right most selector fast path.
{"#A", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple.two", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple", true, 2, {2, 2, 0, 0, 0, 0, 0}},
{"span#multiple", true, 1, {2, 2, 0, 0, 0, 0, 0}},
{"#multiple.two", true, 2, {2, 2, 0, 0, 0, 0, 0}},
{"body #multiple", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"body span#multiple", false, 1, {2, 2, 0, 0, 0, 0, 0}},
{"body #multiple", true, 2, {2, 2, 0, 0, 0, 0, 0}},
{"[id=multiple]", true, 2, {2, 2, 0, 0, 0, 0, 0}},
{"body [id=multiple]", true, 2, {2, 2, 0, 0, 0, 0, 0}},
// Single selector tag fast path.
{"span", false, 1, {4, 0, 0, 4, 0, 0, 0}},
{"span", true, 9, {14, 0, 0, 14, 0, 0, 0}},
// Single selector class fast path.
{".two", false, 1, {6, 0, 6, 0, 0, 0, 0}},
{".two", true, 4, {14, 0, 14, 0, 0, 0, 0}},
// Class in the right most selector fast path.
{"body .two", false, 1, {6, 0, 6, 0, 0, 0, 0}},
{"div .two", false, 1, {12, 0, 12, 0, 0, 0, 0}},
// Classes in the right most selector for querySelectorAll use a fast
// path.
{"body .two", true, 4, {14, 0, 14, 0, 0, 0, 0}},
{"div .two", true, 2, {14, 0, 14, 0, 0, 0, 0}},
// TODO: We could use the fast class path to find the elements inside
// the id scope instead of the fast scan.
{"#second .two", false, 1, {3, 1, 0, 0, 2, 0, 0}},
{"#second .two", true, 2, {5, 1, 0, 0, 4, 0, 0}},
// We combine the class fast path with the fast scan mode when possible.
{".B span", false, 1, {11, 0, 10, 0, 1, 0, 0}},
{".B span", true, 4, {14, 0, 10, 0, 4, 0, 0}},
// We expand the scope of id selectors when affected by an adjectent
// combinator.
{"#c + :last-child", false, 1, {5, 1, 0, 0, 4, 0, 0}},
{"#a ~ :last-child", false, 1, {5, 1, 0, 0, 4, 0, 0}},
{"#c + div", true, 1, {5, 1, 0, 0, 4, 0, 0}},
{"#a ~ span", true, 2, {5, 1, 0, 0, 4, 0, 0}},
// We only expand the scope for id selectors if they're directly affected
// the adjacent combinator.
{"#first span + span", false, 1, {3, 1, 0, 0, 2, 0, 0}},
{"#first span ~ span", false, 1, {3, 1, 0, 0, 2, 0, 0}},
{"#second span + span", true, 3, {5, 1, 0, 0, 4, 0, 0}},
{"#second span ~ span", true, 3, {5, 1, 0, 0, 4, 0, 0}},
// We disable the fast path for class selectors when affected by adjacent
// combinator.
{".one + :last-child", false, 1, {8, 0, 0, 0, 8, 0, 0}},
{".A ~ :last-child", false, 1, {9, 0, 0, 0, 9, 0, 0}},
{".A + div", true, 1, {14, 0, 0, 0, 14, 0, 0}},
{".one ~ span", true, 5, {14, 0, 0, 0, 14, 0, 0}},
// We re-enable the fast path for classes once past the selector directly
// affected by the adjacent combinator.
{".B span + span", true, 3, {14, 0, 10, 0, 4, 0, 0}},
{".B span ~ span", true, 3, {14, 0, 10, 0, 4, 0, 0}},
// Selectors with no classes or ids use the fast scan.
{":scope", false, 1, {1, 0, 0, 0, 1, 0, 0}},
{":scope", true, 1, {14, 0, 0, 0, 14, 0, 0}},
{"foo bar", false, 0, {14, 0, 0, 0, 14, 0, 0}},
// Multiple selectors always uses the slow path.
// TODO(esprehn): We could make this fast if we sorted the output, not
// sure it's worth it unless we're dealing with ids.
{"#a, #b", false, 1, {5, 0, 0, 0, 0, 5, 0}},
{"#a, #b", true, 2, {14, 0, 0, 0, 0, 14, 0}},
// Anything that crosses shadow boundaries is slow path.
{"#foo /deep/ .a", false, 0, {14, 0, 0, 0, 0, 0, 14}},
{"#foo::shadow .a", false, 0, {14, 0, 0, 0, 0, 0, 14}},
{"::content .a", false, 0, {14, 0, 0, 0, 0, 14, 0}},
{"#foo /deep/ .a", true, 0, {14, 0, 0, 0, 0, 0, 14}},
{"#foo::shadow .a", true, 0, {14, 0, 0, 0, 0, 0, 14}},
{"::content .a", true, 0, {14, 0, 0, 0, 0, 14, 0}},
};
RunTests(*document, kTestCases);
}
TEST(SelectorQueryTest, FastPathScoped) {
Document* document = HTMLDocument::CreateForTest();
document->write(R"HTML(
<!DOCTYPE html>
<html id=root-id class=root-class>
<head></head>
<body>
<span id=first>
<span id=A class='a child'></span>
<span id=B class='a child'>
<a class=first></a>
<a class=second></a>
<a class=third></a>
</span>
<span id=multiple class='b child'></span>
<span id=multiple class='c child'></span>
</span>
</body>
</html>
)HTML");
Element* scope = document->getElementById("first");
ASSERT_NE(nullptr, scope);
ShadowRoot& shadowRoot =
scope->AttachShadowRootInternal(ShadowRootType::kOpen);
// Make the inside the shadow root be identical to that of the outer document.
shadowRoot.appendChild(&document->documentElement()->CloneWithChildren());
static const struct QueryTest kTestCases[] = {
// Id in the right most selector.
{"#first", false, 0, {0, 0, 0, 0, 0, 0, 0}},
{"#B", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple.c", false, 1, {2, 2, 0, 0, 0, 0, 0}},
// Class in the right most selector.
{".child", false, 1, {1, 0, 1, 0, 0, 0, 0}},
{".child", true, 4, {7, 0, 7, 0, 0, 0, 0}},
// If an ancestor has the class name we fast scan all the descendants of
// the scope.
{".root-class span", true, 4, {7, 0, 0, 0, 7, 0, 0}},
// If an ancestor has the class name in the middle of the selector we fast
// scan all the descendants of the scope.
{".root-class span:nth-child(2)", false, 1, {2, 0, 0, 0, 2, 0, 0}},
{".root-class span:nth-child(2)", true, 1, {7, 0, 0, 0, 7, 0, 0}},
// If the id is an ancestor we scan all the descendants.
{"#root-id span", true, 4, {8, 1, 0, 0, 7, 0, 0}},
};
{
SCOPED_TRACE("Inside document");
RunTests(*scope, kTestCases);
}
{
// Run all the tests a second time but with a scope inside a shadow root,
// all the fast paths should behave the same.
SCOPED_TRACE("Inside shadow root");
scope = shadowRoot.getElementById("first");
ASSERT_NE(nullptr, scope);
RunTests(*scope, kTestCases);
}
}
TEST(SelectorQueryTest, QuirksModeSlowPath) {
Document* document = HTMLDocument::CreateForTest();
document->write(R"HTML(
<html>
<head></head>
<body>
<span id=first>
<span id=One class=Two></span>
<span id=one class=tWo></span>
</span>
</body>
</html>
)HTML");
static const struct QueryTest kTestCases[] = {
// Quirks mode can't use the id fast path due to being case-insensitive.
{"#one", false, 1, {5, 0, 0, 0, 5, 0, 0}},
{"#One", false, 1, {5, 0, 0, 0, 5, 0, 0}},
{"#ONE", false, 1, {5, 0, 0, 0, 5, 0, 0}},
{"#ONE", true, 2, {6, 0, 0, 0, 6, 0, 0}},
{"[id=One]", false, 1, {5, 0, 0, 0, 5, 0, 0}},
{"[id=One]", true, 1, {6, 0, 0, 0, 6, 0, 0}},
{"body #first", false, 1, {4, 0, 0, 0, 4, 0, 0}},
{"body #one", true, 2, {6, 0, 0, 0, 6, 0, 0}},
// Quirks can use the class and tag name fast paths though.
{"span", false, 1, {4, 0, 0, 4, 0, 0, 0}},
{"span", true, 3, {6, 0, 0, 6, 0, 0, 0}},
{".two", false, 1, {5, 0, 5, 0, 0, 0, 0}},
{".two", true, 2, {6, 0, 6, 0, 0, 0, 0}},
{"body span", false, 1, {4, 0, 0, 0, 4, 0, 0}},
{"body span", true, 3, {6, 0, 0, 0, 6, 0, 0}},
{"body .two", false, 1, {5, 0, 5, 0, 0, 0, 0}},
{"body .two", true, 2, {6, 0, 6, 0, 0, 0, 0}},
};
RunTests(*document, kTestCases);
}
TEST(SelectorQueryTest, DisconnectedSubtree) {
Document* document = HTMLDocument::CreateForTest();
Element* scope = document->CreateRawElement(html_names::kDivTag);
scope->SetInnerHTMLFromString(R"HTML(
<section>
<span id=first>
<span id=A class=A></span>
<span id=B class=child></span>
<span id=multiple class=child></span>
<span id=multiple class=B></span>
</span>
</section>
)HTML");
static const struct QueryTest kTestCases[] = {
{"#A", false, 1, {3, 0, 0, 0, 3, 0, 0}},
{"#B", false, 1, {4, 0, 0, 0, 4, 0, 0}},
{"#B", true, 1, {6, 0, 0, 0, 6, 0, 0}},
{"#multiple", true, 2, {6, 0, 0, 0, 6, 0, 0}},
{".child", false, 1, {4, 0, 4, 0, 0, 0, 0}},
{".child", true, 2, {6, 0, 6, 0, 0, 0, 0}},
{"#first span", false, 1, {3, 0, 0, 0, 3, 0, 0}},
{"#first span", true, 4, {6, 0, 0, 0, 6, 0, 0}},
};
RunTests(*scope, kTestCases);
}
TEST(SelectorQueryTest, DisconnectedTreeScope) {
Document* document = HTMLDocument::CreateForTest();
Element* host = document->CreateRawElement(html_names::kDivTag);
ShadowRoot& shadowRoot =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadowRoot.SetInnerHTMLFromString(R"HTML(
<section>
<span id=first>
<span id=A class=A></span>
<span id=B class=child></span>
<span id=multiple class=child></span>
<span id=multiple class=B></span>
</span>
</section>
)HTML");
static const struct QueryTest kTestCases[] = {
{"#A", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#B", false, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#B", true, 1, {1, 1, 0, 0, 0, 0, 0}},
{"#multiple", true, 2, {2, 2, 0, 0, 0, 0, 0}},
{".child", false, 1, {4, 0, 4, 0, 0, 0, 0}},
{".child", true, 2, {6, 0, 6, 0, 0, 0, 0}},
{"#first span", false, 1, {2, 1, 0, 0, 1, 0, 0}},
{"#first span", true, 4, {5, 1, 0, 0, 4, 0, 0}},
};
RunTests(shadowRoot, kTestCases);
}
} // namespace blink