| // Copyright 2014 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 "core/HTMLNames.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/ElementTraversal.h" |
| #include "core/dom/NodeComputedStyle.h" |
| #include "core/dom/StyleEngine.h" |
| #include "core/frame/FrameView.h" |
| #include "core/html/HTMLElement.h" |
| #include "core/testing/DummyPageHolder.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include <memory> |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| class AffectedByFocusTest : public ::testing::Test { |
| protected: |
| struct ElementResult { |
| const blink::HTMLQualifiedName tag; |
| bool affectedBy; |
| bool childrenOrSiblingsAffectedBy; |
| }; |
| |
| void SetUp() override; |
| |
| Document& document() const { return *m_document; } |
| |
| void setHtmlInnerHTML(const char* htmlContent); |
| |
| void checkElements(ElementResult expected[], unsigned expectedCount) const; |
| |
| private: |
| std::unique_ptr<DummyPageHolder> m_dummyPageHolder; |
| |
| Persistent<Document> m_document; |
| }; |
| |
| void AffectedByFocusTest::SetUp() { |
| m_dummyPageHolder = DummyPageHolder::create(IntSize(800, 600)); |
| m_document = &m_dummyPageHolder->document(); |
| ASSERT(m_document); |
| } |
| |
| void AffectedByFocusTest::setHtmlInnerHTML(const char* htmlContent) { |
| document().documentElement()->setInnerHTML(String::fromUTF8(htmlContent), |
| ASSERT_NO_EXCEPTION); |
| document().view()->updateAllLifecyclePhases(); |
| } |
| |
| void AffectedByFocusTest::checkElements(ElementResult expected[], |
| unsigned expectedCount) const { |
| unsigned i = 0; |
| HTMLElement* element = document().body(); |
| |
| for (; element && i < expectedCount; |
| element = Traversal<HTMLElement>::next(*element), ++i) { |
| ASSERT_TRUE(element->hasTagName(expected[i].tag)); |
| ASSERT(element->computedStyle()); |
| ASSERT_EQ(expected[i].affectedBy, |
| element->computedStyle()->affectedByFocus()); |
| ASSERT_EQ(expected[i].childrenOrSiblingsAffectedBy, |
| element->childrenOrSiblingsAffectedByFocus()); |
| } |
| |
| ASSERT(!element && i == expectedCount); |
| } |
| |
| // A global :focus rule in html.css currently causes every single element to be |
| // affectedByFocus. Check that all elements in a document with no :focus rules |
| // gets the affectedByFocus set on ComputedStyle and not |
| // childrenOrSiblingsAffectedByFocus. |
| TEST_F(AffectedByFocusTest, UAUniversalFocusRule) { |
| ElementResult expected[] = {{bodyTag, true, false}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {spanTag, true, false}}; |
| |
| setHtmlInnerHTML( |
| "<body>" |
| "<div><div></div></div>" |
| "<div><span></span></div>" |
| "</body>"); |
| |
| checkElements(expected, sizeof(expected) / sizeof(ElementResult)); |
| } |
| |
| // ":focus div" will mark ascendants of all divs with |
| // childrenOrSiblingsAffectedByFocus. |
| TEST_F(AffectedByFocusTest, FocusedAscendant) { |
| ElementResult expected[] = {{bodyTag, true, true}, |
| {divTag, true, true}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {spanTag, true, false}}; |
| |
| setHtmlInnerHTML( |
| "<head>" |
| "<style>:focus div { background-color: pink }</style>" |
| "</head>" |
| "<body>" |
| "<div><div></div></div>" |
| "<div><span></span></div>" |
| "</body>"); |
| |
| checkElements(expected, sizeof(expected) / sizeof(ElementResult)); |
| } |
| |
| // "body:focus div" will mark the body element with |
| // childrenOrSiblingsAffectedByFocus. |
| TEST_F(AffectedByFocusTest, FocusedAscendantWithType) { |
| ElementResult expected[] = {{bodyTag, true, true}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {spanTag, true, false}}; |
| |
| setHtmlInnerHTML( |
| "<head>" |
| "<style>body:focus div { background-color: pink }</style>" |
| "</head>" |
| "<body>" |
| "<div><div></div></div>" |
| "<div><span></span></div>" |
| "</body>"); |
| |
| checkElements(expected, sizeof(expected) / sizeof(ElementResult)); |
| } |
| |
| // ":not(body):focus div" should not mark the body element with |
| // childrenOrSiblingsAffectedByFocus. |
| // Note that currently ":focus:not(body)" does not do the same. Then the :focus |
| // is checked and the childrenOrSiblingsAffectedByFocus flag set before the |
| // negated type selector is found. |
| TEST_F(AffectedByFocusTest, FocusedAscendantWithNegatedType) { |
| ElementResult expected[] = {{bodyTag, true, false}, |
| {divTag, true, true}, |
| {divTag, true, false}, |
| {divTag, true, false}, |
| {spanTag, true, false}}; |
| |
| setHtmlInnerHTML( |
| "<head>" |
| "<style>:not(body):focus div { background-color: pink }</style>" |
| "</head>" |
| "<body>" |
| "<div><div></div></div>" |
| "<div><span></span></div>" |
| "</body>"); |
| |
| checkElements(expected, sizeof(expected) / sizeof(ElementResult)); |
| } |
| |
| // Checking current behavior for ":focus + div", but this is a BUG or at best |
| // sub-optimal. The focused element will also in this case get |
| // childrenOrSiblingsAffectedByFocus even if it's really a sibling. Effectively, |
| // the whole sub-tree of the focused element will have styles recalculated even |
| // though none of the children are affected. There are other mechanisms that |
| // makes sure the sibling also gets its styles recalculated. |
| TEST_F(AffectedByFocusTest, FocusedSibling) { |
| ElementResult expected[] = {{bodyTag, true, false}, |
| {divTag, true, true}, |
| {spanTag, true, false}, |
| {divTag, true, false}}; |
| |
| setHtmlInnerHTML( |
| "<head>" |
| "<style>:focus + div { background-color: pink }</style>" |
| "</head>" |
| "<body>" |
| "<div>" |
| " <span></span>" |
| "</div>" |
| "<div></div>" |
| "</body>"); |
| |
| checkElements(expected, sizeof(expected) / sizeof(ElementResult)); |
| } |
| |
| TEST_F(AffectedByFocusTest, AffectedByFocusUpdate) { |
| // Check that when focussing the outer div in the document below, you only |
| // get a single element style recalc. |
| |
| setHtmlInnerHTML( |
| "<style>:focus { border: 1px solid lime; }</style>" |
| "<div id=d tabIndex=1>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "</div>"); |
| |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned startCount = document().styleEngine().styleForElementCount(); |
| |
| document().getElementById("d")->focus(); |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned elementCount = |
| document().styleEngine().styleForElementCount() - startCount; |
| |
| ASSERT_EQ(1U, elementCount); |
| } |
| |
| TEST_F(AffectedByFocusTest, ChildrenOrSiblingsAffectedByFocusUpdate) { |
| // Check that when focussing the outer div in the document below, you get a |
| // style recalc for the whole subtree. |
| |
| setHtmlInnerHTML( |
| "<style>:focus div { border: 1px solid lime; }</style>" |
| "<div id=d tabIndex=1>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "</div>"); |
| |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned startCount = document().styleEngine().styleForElementCount(); |
| |
| document().getElementById("d")->focus(); |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned elementCount = |
| document().styleEngine().styleForElementCount() - startCount; |
| |
| ASSERT_EQ(11U, elementCount); |
| } |
| |
| TEST_F(AffectedByFocusTest, InvalidationSetFocusUpdate) { |
| // Check that when focussing the outer div in the document below, you get a |
| // style recalc for the outer div and the class=a div only. |
| |
| setHtmlInnerHTML( |
| "<style>:focus .a { border: 1px solid lime; }</style>" |
| "<div id=d tabIndex=1>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div class='a'></div>" |
| "</div>"); |
| |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned startCount = document().styleEngine().styleForElementCount(); |
| |
| document().getElementById("d")->focus(); |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned elementCount = |
| document().styleEngine().styleForElementCount() - startCount; |
| |
| ASSERT_EQ(2U, elementCount); |
| } |
| |
| TEST_F(AffectedByFocusTest, NoInvalidationSetFocusUpdate) { |
| // Check that when focussing the outer div in the document below, you get a |
| // style recalc for the outer div only. The invalidation set for :focus will |
| // include 'a', but the id=d div should be affectedByFocus, not |
| // childrenOrSiblingsAffectedByFocus. |
| |
| setHtmlInnerHTML( |
| "<style>#nomatch:focus .a { border: 1px solid lime; }</style>" |
| "<div id=d tabIndex=1>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div></div>" |
| "<div class='a'></div>" |
| "</div>"); |
| |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned startCount = document().styleEngine().styleForElementCount(); |
| |
| document().getElementById("d")->focus(); |
| document().view()->updateAllLifecyclePhases(); |
| |
| unsigned elementCount = |
| document().styleEngine().styleForElementCount() - startCount; |
| |
| ASSERT_EQ(1U, elementCount); |
| } |
| |
| } // namespace blink |