| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "core/dom/SelectorQuery.h" | 5 #include "core/dom/SelectorQuery.h" |
| 6 | 6 |
| 7 #include <memory> | 7 #include <memory> |
| 8 #include "core/css/parser/CSSParser.h" | 8 #include "core/css/parser/CSSParser.h" |
| 9 #include "core/css/parser/CSSParserContext.h" | 9 #include "core/css/parser/CSSParserContext.h" |
| 10 #include "core/dom/Document.h" | 10 #include "core/dom/Document.h" |
| 11 #include "core/dom/ElementTraversal.h" |
| 11 #include "core/dom/StaticNodeList.h" | 12 #include "core/dom/StaticNodeList.h" |
| 13 #include "core/dom/shadow/ElementShadow.h" |
| 12 #include "core/html/HTMLDocument.h" | 14 #include "core/html/HTMLDocument.h" |
| 13 #include "core/html/HTMLHtmlElement.h" | 15 #include "core/html/HTMLHtmlElement.h" |
| 14 #include "testing/gtest/include/gtest/gtest.h" | 16 #include "testing/gtest/include/gtest/gtest.h" |
| 15 | 17 |
| 16 namespace blink { | 18 namespace blink { |
| 17 | 19 |
| 18 namespace { | 20 namespace { |
| 19 struct QueryTest { | 21 struct QueryTest { |
| 20 const char* selector; | 22 const char* selector; |
| 21 bool query_all; | 23 bool query_all; |
| 22 unsigned matches; | 24 unsigned matches; |
| 23 // {totalCount, fastId, fastClass, fastTagName, fastScan, slowScan, | 25 // {totalCount, fastId, fastClass, fastTagName, fastScan, slowScan, |
| 24 // slowTraversingShadowTreeScan} | 26 // slowTraversingShadowTreeScan} |
| 25 SelectorQuery::QueryStats stats; | 27 SelectorQuery::QueryStats stats; |
| 26 }; | 28 }; |
| 27 | 29 |
| 28 template <unsigned length> | 30 template <unsigned length> |
| 29 void RunTests(Document& document, const QueryTest (&test_cases)[length]) { | 31 void RunTests(ContainerNode& scope, const QueryTest (&test_cases)[length]) { |
| 30 for (const auto& test_case : test_cases) { | 32 for (const auto& test_case : test_cases) { |
| 31 const char* selector = test_case.selector; | 33 const char* selector = test_case.selector; |
| 32 SCOPED_TRACE(testing::Message() | 34 SCOPED_TRACE(testing::Message() |
| 33 << (test_case.query_all ? "querySelectorAll('" | 35 << (test_case.query_all ? "querySelectorAll('" |
| 34 : "querySelector('") | 36 : "querySelector('") |
| 35 << selector << "')"); | 37 << selector << "')"); |
| 36 if (test_case.query_all) { | 38 if (test_case.query_all) { |
| 37 StaticElementList* match_all = document.QuerySelectorAll(selector); | 39 StaticElementList* match_all = scope.QuerySelectorAll(selector); |
| 38 EXPECT_EQ(test_case.matches, match_all->length()); | 40 EXPECT_EQ(test_case.matches, match_all->length()); |
| 39 } else { | 41 } else { |
| 40 Element* match = document.QuerySelector(selector); | 42 Element* match = scope.QuerySelector(selector); |
| 41 EXPECT_EQ(test_case.matches, match ? 1u : 0u); | 43 EXPECT_EQ(test_case.matches, match ? 1u : 0u); |
| 42 } | 44 } |
| 43 #if DCHECK_IS_ON() | 45 #if DCHECK_IS_ON() |
| 44 SelectorQuery::QueryStats stats = SelectorQuery::LastQueryStats(); | 46 SelectorQuery::QueryStats stats = SelectorQuery::LastQueryStats(); |
| 45 EXPECT_EQ(test_case.stats.total_count, stats.total_count); | 47 EXPECT_EQ(test_case.stats.total_count, stats.total_count); |
| 46 EXPECT_EQ(test_case.stats.fast_id, stats.fast_id); | 48 EXPECT_EQ(test_case.stats.fast_id, stats.fast_id); |
| 47 EXPECT_EQ(test_case.stats.fast_class, stats.fast_class); | 49 EXPECT_EQ(test_case.stats.fast_class, stats.fast_class); |
| 48 EXPECT_EQ(test_case.stats.fast_tag_name, stats.fast_tag_name); | 50 EXPECT_EQ(test_case.stats.fast_tag_name, stats.fast_tag_name); |
| 49 EXPECT_EQ(test_case.stats.fast_scan, stats.fast_scan); | 51 EXPECT_EQ(test_case.stats.fast_scan, stats.fast_scan); |
| 50 EXPECT_EQ(test_case.stats.slow_scan, stats.slow_scan); | 52 EXPECT_EQ(test_case.stats.slow_scan, stats.slow_scan); |
| (...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 156 {"div .two", true, 2, {14, 0, 14, 0, 0, 0, 0}}, | 158 {"div .two", true, 2, {14, 0, 14, 0, 0, 0, 0}}, |
| 157 | 159 |
| 158 // TODO: querySelector disables the class fast path to favor the id, but | 160 // TODO: querySelector disables the class fast path to favor the id, but |
| 159 // this means some selectors always end up doing fastScan. | 161 // this means some selectors always end up doing fastScan. |
| 160 {"#second .two", false, 1, {2, 0, 0, 0, 2, 0, 0}}, | 162 {"#second .two", false, 1, {2, 0, 0, 0, 2, 0, 0}}, |
| 161 | 163 |
| 162 // TODO(esprehn): This should have used getElementById instead of doing | 164 // TODO(esprehn): This should have used getElementById instead of doing |
| 163 // a fastClass scan. It could have looked at 4 elements instead. | 165 // a fastClass scan. It could have looked at 4 elements instead. |
| 164 {"#second .two", true, 2, {14, 0, 14, 0, 0, 0, 0}}, | 166 {"#second .two", true, 2, {14, 0, 14, 0, 0, 0, 0}}, |
| 165 | 167 |
| 168 // We combine the class fast path with the fast scan mode when possible. |
| 169 {".B span", false, 1, {11, 0, 0, 0, 11, 0, 0}}, |
| 170 {".B span", true, 4, {14, 0, 10, 0, 4, 0, 0}}, |
| 171 |
| 172 // We expand the scope of id selectors when affected by an adjectent |
| 173 // combinator. |
| 174 {"#c + :last-child", false, 1, {4, 0, 0, 0, 4, 0, 0}}, |
| 175 {"#a ~ :last-child", false, 1, {4, 0, 0, 0, 4, 0, 0}}, |
| 176 {"#c + div", true, 1, {4, 0, 0, 0, 4, 0, 0}}, |
| 177 {"#a ~ span", true, 2, {4, 0, 0, 0, 4, 0, 0}}, |
| 178 |
| 179 // We only expand the scope for id selectors if they're directly affected |
| 180 // the adjacent combinator. |
| 181 {"#first span + span", false, 1, {2, 0, 0, 0, 2, 0, 0}}, |
| 182 {"#first span ~ span", false, 1, {2, 0, 0, 0, 2, 0, 0}}, |
| 183 {"#second span + span", true, 3, {4, 0, 0, 0, 4, 0, 0}}, |
| 184 {"#second span ~ span", true, 3, {4, 0, 0, 0, 4, 0, 0}}, |
| 185 |
| 186 // We disable the fast path for class selectors when affected by adjacent |
| 187 // combinator. |
| 188 {".one + :last-child", false, 1, {8, 0, 0, 0, 8, 0, 0}}, |
| 189 {".A ~ :last-child", false, 1, {9, 0, 0, 0, 9, 0, 0}}, |
| 190 {".A + div", true, 1, {14, 0, 0, 0, 14, 0, 0}}, |
| 191 {".one ~ span", true, 5, {14, 0, 0, 0, 14, 0, 0}}, |
| 192 |
| 193 // We re-enable the fast path for classes once past the selector directly |
| 194 // affected by the adjacent combinator. |
| 195 {".B span + span", true, 3, {14, 0, 10, 0, 4, 0, 0}}, |
| 196 {".B span ~ span", true, 3, {14, 0, 10, 0, 4, 0, 0}}, |
| 197 |
| 166 // Selectors with no classes or ids use the fast scan. | 198 // Selectors with no classes or ids use the fast scan. |
| 167 {":scope", false, 1, {1, 0, 0, 0, 1, 0, 0}}, | 199 {":scope", false, 1, {1, 0, 0, 0, 1, 0, 0}}, |
| 168 {":scope", true, 1, {14, 0, 0, 0, 14, 0, 0}}, | 200 {":scope", true, 1, {14, 0, 0, 0, 14, 0, 0}}, |
| 169 {"foo bar", false, 0, {14, 0, 0, 0, 14, 0, 0}}, | 201 {"foo bar", false, 0, {14, 0, 0, 0, 14, 0, 0}}, |
| 170 | 202 |
| 171 // Multiple selectors always uses the slow path. | 203 // Multiple selectors always uses the slow path. |
| 172 // TODO(esprehn): We could make this fast if we sorted the output, not | 204 // TODO(esprehn): We could make this fast if we sorted the output, not |
| 173 // sure it's worth it unless we're dealing with ids. | 205 // sure it's worth it unless we're dealing with ids. |
| 174 {"#a, #b", false, 1, {5, 0, 0, 0, 0, 5, 0}}, | 206 {"#a, #b", false, 1, {5, 0, 0, 0, 0, 5, 0}}, |
| 175 {"#a, #b", true, 2, {14, 0, 0, 0, 0, 14, 0}}, | 207 {"#a, #b", true, 2, {14, 0, 0, 0, 0, 14, 0}}, |
| 176 | 208 |
| 177 // Anything that crosses shadow boundaries is slow path. | 209 // Anything that crosses shadow boundaries is slow path. |
| 178 {"#foo /deep/ .a", false, 0, {14, 0, 0, 0, 0, 0, 14}}, | 210 {"#foo /deep/ .a", false, 0, {14, 0, 0, 0, 0, 0, 14}}, |
| 179 {"#foo::shadow .a", false, 0, {14, 0, 0, 0, 0, 0, 14}}, | 211 {"#foo::shadow .a", false, 0, {14, 0, 0, 0, 0, 0, 14}}, |
| 180 {"::content .a", false, 0, {14, 0, 0, 0, 0, 14, 0}}, | 212 {"::content .a", false, 0, {14, 0, 0, 0, 0, 14, 0}}, |
| 181 {"#foo /deep/ .a", true, 0, {14, 0, 0, 0, 0, 0, 14}}, | 213 {"#foo /deep/ .a", true, 0, {14, 0, 0, 0, 0, 0, 14}}, |
| 182 {"#foo::shadow .a", true, 0, {14, 0, 0, 0, 0, 0, 14}}, | 214 {"#foo::shadow .a", true, 0, {14, 0, 0, 0, 0, 0, 14}}, |
| 183 {"::content .a", true, 0, {14, 0, 0, 0, 0, 14, 0}}, | 215 {"::content .a", true, 0, {14, 0, 0, 0, 0, 14, 0}}, |
| 184 }; | 216 }; |
| 185 RunTests(*document, kTestCases); | 217 RunTests(*document, kTestCases); |
| 186 } | 218 } |
| 187 | 219 |
| 220 TEST(SelectorQueryTest, FastPathScoped) { |
| 221 Document* document = HTMLDocument::Create(); |
| 222 document->write( |
| 223 "<!DOCTYPE html>" |
| 224 "<html id=root-id class=root-class>" |
| 225 " <head></head>" |
| 226 " <body>" |
| 227 " <span id=first>" |
| 228 " <span id=A class='a child'></span>" |
| 229 " <span id=B class='a child'>" |
| 230 " <a class=first></a>" |
| 231 " <a class=second></a>" |
| 232 " <a class=third></a>" |
| 233 " </span>" |
| 234 " <span id=multiple class='b child'></span>" |
| 235 " <span id=multiple class='c child'></span>" |
| 236 " </span>" |
| 237 " </body>" |
| 238 "</html>"); |
| 239 // TODO(esprehn): Element::attachShadow() should not require a ScriptState, |
| 240 // it should handle the use counting in the bindings layer instead of in the |
| 241 // C++. |
| 242 Element* scope = document->getElementById("first"); |
| 243 ASSERT_NE(nullptr, scope); |
| 244 ShadowRoot& shadowRoot = |
| 245 scope->EnsureShadow().AddShadowRoot(*scope, ShadowRootType::kOpen); |
| 246 // Make the inside the shadow root be identical to that of the outer document. |
| 247 shadowRoot.appendChild( |
| 248 document->documentElement()->CloneElementWithChildren()); |
| 249 static const struct QueryTest kTestCases[] = { |
| 250 // Id in the right most selector. |
| 251 {"#first", false, 0, {0, 0, 0, 0, 0, 0, 0}}, |
| 252 |
| 253 {"#B", false, 1, {1, 1, 0, 0, 0, 0, 0}}, |
| 254 {"#multiple", false, 1, {1, 1, 0, 0, 0, 0, 0}}, |
| 255 {"#multiple.c", false, 1, {2, 2, 0, 0, 0, 0, 0}}, |
| 256 |
| 257 // Class in the right most selector. |
| 258 {".child", false, 1, {1, 0, 1, 0, 0, 0, 0}}, |
| 259 {".child", true, 4, {7, 0, 7, 0, 0, 0, 0}}, |
| 260 |
| 261 // If an ancestor has the class name we fast scan all the descendants of |
| 262 // the scope. |
| 263 {".root-class span", true, 4, {7, 0, 0, 0, 7, 0, 0}}, |
| 264 |
| 265 // If an ancestor has the class name in the middle of the selector we fast |
| 266 // scan all the descendants of the scope. |
| 267 {".root-class span:nth-child(2)", false, 1, {2, 0, 0, 0, 2, 0, 0}}, |
| 268 {".root-class span:nth-child(2)", true, 1, {7, 0, 0, 0, 7, 0, 0}}, |
| 269 |
| 270 // If the id is an ancestor we scan all the descendants. |
| 271 {"#root-id span", true, 4, {7, 0, 0, 0, 7, 0, 0}}, |
| 272 }; |
| 273 |
| 274 { |
| 275 SCOPED_TRACE("Inside document"); |
| 276 RunTests(*scope, kTestCases); |
| 277 } |
| 278 |
| 279 { |
| 280 // Run all the tests a second time but with a scope inside a shadow root, |
| 281 // all the fast paths should behave the same. |
| 282 SCOPED_TRACE("Inside shadow root"); |
| 283 scope = shadowRoot.getElementById("first"); |
| 284 ASSERT_NE(nullptr, scope); |
| 285 RunTests(*scope, kTestCases); |
| 286 } |
| 287 } |
| 288 |
| 188 TEST(SelectorQueryTest, QuirksModeSlowPath) { | 289 TEST(SelectorQueryTest, QuirksModeSlowPath) { |
| 189 Document* document = HTMLDocument::Create(); | 290 Document* document = HTMLDocument::Create(); |
| 190 document->write( | 291 document->write( |
| 191 "<html>" | 292 "<html>" |
| 192 " <head></head>" | 293 " <head></head>" |
| 193 " <body>" | 294 " <body>" |
| 194 " <span id=first>" | 295 " <span id=first>" |
| 195 " <span id=One class=Two></span>" | 296 " <span id=One class=Two></span>" |
| 196 " <span id=one class=tWo></span>" | 297 " <span id=one class=tWo></span>" |
| 197 " </span>" | 298 " </span>" |
| 198 " </body>" | 299 " </body>" |
| 199 "</html>"); | 300 "</html>"); |
| 200 static const struct QueryTest kTestCases[] = { | 301 static const struct QueryTest kTestCases[] = { |
| 201 // Quirks mode always uses the slow path. | 302 // Quirks mode always uses the slow path. |
| 202 {"#one", false, 1, {5, 0, 0, 0, 0, 5, 0}}, | 303 {"#one", false, 1, {5, 0, 0, 0, 0, 5, 0}}, |
| 203 {"#One", false, 1, {5, 0, 0, 0, 0, 5, 0}}, | 304 {"#One", false, 1, {5, 0, 0, 0, 0, 5, 0}}, |
| 204 {"#ONE", false, 1, {5, 0, 0, 0, 0, 5, 0}}, | 305 {"#ONE", false, 1, {5, 0, 0, 0, 0, 5, 0}}, |
| 205 {"#ONE", true, 2, {6, 0, 0, 0, 0, 6, 0}}, | 306 {"#ONE", true, 2, {6, 0, 0, 0, 0, 6, 0}}, |
| 206 {"span", false, 1, {4, 0, 0, 0, 0, 4, 0}}, | 307 {"span", false, 1, {4, 0, 0, 0, 0, 4, 0}}, |
| 207 {"span", true, 3, {6, 0, 0, 0, 0, 6, 0}}, | 308 {"span", true, 3, {6, 0, 0, 0, 0, 6, 0}}, |
| 208 {".two", false, 1, {5, 0, 0, 0, 0, 5, 0}}, | 309 {".two", false, 1, {5, 0, 0, 0, 0, 5, 0}}, |
| 209 {".two", true, 2, {6, 0, 0, 0, 0, 6, 0}}, | 310 {".two", true, 2, {6, 0, 0, 0, 0, 6, 0}}, |
| 210 {"body #first", false, 1, {4, 0, 0, 0, 0, 4, 0}}, | 311 {"body #first", false, 1, {4, 0, 0, 0, 0, 4, 0}}, |
| 211 {"body #one", true, 2, {6, 0, 0, 0, 0, 6, 0}}, | 312 {"body #one", true, 2, {6, 0, 0, 0, 0, 6, 0}}, |
| 212 }; | 313 }; |
| 213 RunTests(*document, kTestCases); | 314 RunTests(*document, kTestCases); |
| 214 } | 315 } |
| 215 | 316 |
| 317 TEST(SelectorQueryTest, DisconnectedSubtreeSlowPath) { |
| 318 Document* document = HTMLDocument::Create(); |
| 319 Element* scope = document->createElement("div"); |
| 320 scope->setInnerHTML( |
| 321 "<section>" |
| 322 " <span id=first>" |
| 323 " <span id=A class=A></span>" |
| 324 " <span id=B class=child></span>" |
| 325 " <span id=multiple class=child></span>" |
| 326 " <span id=multiple class=B></span>" |
| 327 " </span>" |
| 328 "</section>"); |
| 329 ShadowRoot& shadowRoot = |
| 330 scope->EnsureShadow().AddShadowRoot(*scope, ShadowRootType::kOpen); |
| 331 // Make the inside the ShadowRoot look identical to the outer document. |
| 332 shadowRoot.appendChild( |
| 333 ElementTraversal::FirstChild(*scope)->CloneElementWithChildren()); |
| 334 static const struct QueryTest kTestCases[] = { |
| 335 // TODO(esprehn): Disconnected subtrees always uses the slow path, but |
| 336 // we can actually use it in a number of cases, for example using the id |
| 337 // map for things inside a tree scope, or using the fast class scanning |
| 338 // always. |
| 339 {"#A", false, 1, {3, 0, 0, 0, 0, 3, 0}}, |
| 340 {"#B", false, 1, {4, 0, 0, 0, 0, 4, 0}}, |
| 341 {"#B", true, 1, {6, 0, 0, 0, 0, 6, 0}}, |
| 342 {"#multiple", true, 2, {6, 0, 0, 0, 0, 6, 0}}, |
| 343 {".child", false, 1, {4, 0, 0, 0, 0, 4, 0}}, |
| 344 {".child", true, 2, {6, 0, 0, 0, 0, 6, 0}}, |
| 345 {"#first span", false, 1, {3, 0, 0, 0, 0, 3, 0}}, |
| 346 {"#first span", true, 4, {6, 0, 0, 0, 0, 6, 0}}, |
| 347 }; |
| 348 |
| 349 { |
| 350 SCOPED_TRACE("Inside disconnected subtree"); |
| 351 RunTests(*scope, kTestCases); |
| 352 } |
| 353 |
| 354 { |
| 355 // Run all the tests a second time but with a scope inside a shadow root, |
| 356 // this tests for cases where we could have used the id map the ShadowRoot |
| 357 // is keeping track of. |
| 358 SCOPED_TRACE("Inside disconnected shadow root subtree"); |
| 359 RunTests(shadowRoot, kTestCases); |
| 360 } |
| 361 } |
| 362 |
| 216 } // namespace blink | 363 } // namespace blink |
| OLD | NEW |