blob: 9df306c082226b98f1629d60b751061f8bc1b032 [file] [log] [blame]
// Copyright 2015 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/dom/node.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/dom/comment.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/layout_tree_builder.h"
#include "third_party/blink/renderer/core/dom/processing_instruction.h"
#include "third_party/blink/renderer/core/dom/pseudo_element.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/shadow_root_init.h"
#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
namespace blink {
class FakeMediaControlElement : public HTMLDivElement {
public:
FakeMediaControlElement(Document& document) : HTMLDivElement(document) {}
bool IsMediaControlElement() const override { return true; }
};
class FakeMediaControls : public HTMLDivElement {
public:
FakeMediaControls(Document& document) : HTMLDivElement(document) {}
bool IsMediaControls() const override { return true; }
};
class NodeTest : public EditingTestBase {
protected:
LayoutObject* ReattachLayoutTreeForNode(Node& node) {
node.SetForceReattachLayoutTree();
GetDocument().Lifecycle().AdvanceTo(DocumentLifecycle::kInStyleRecalc);
GetDocument().GetStyleEngine().RecalcStyle();
Node::AttachContext context;
context.parent = LayoutTreeBuilderTraversal::ParentLayoutObject(node);
GetDocument().GetStyleEngine().in_layout_tree_rebuild_ = true;
node.ReattachLayoutTree(context);
return context.previous_in_flow;
}
// Generate the following DOM structure and return the innermost <div>.
// + div#root
// + #shadow
// + test node
// | + #shadow
// | + div class="test"
Node* InitializeUserAgentShadowTree(Element* test_node) {
SetBodyContent("<div id=\"root\"></div>");
Element* root = GetDocument().getElementById("root");
ShadowRoot& first_shadow = root->CreateUserAgentShadowRoot();
first_shadow.AppendChild(test_node);
ShadowRoot& second_shadow = test_node->CreateUserAgentShadowRoot();
auto* class_div = MakeGarbageCollected<HTMLDivElement>(GetDocument());
class_div->setAttribute("class", "test");
second_shadow.AppendChild(class_div);
return class_div;
}
};
TEST_F(NodeTest, canStartSelection) {
const char* body_content =
"<a id=one href='http://www.msn.com'>one</a><b id=two>two</b>";
SetBodyContent(body_content);
Node* one = GetDocument().getElementById("one");
Node* two = GetDocument().getElementById("two");
EXPECT_FALSE(one->CanStartSelection());
EXPECT_FALSE(one->firstChild()->CanStartSelection());
EXPECT_TRUE(two->CanStartSelection());
EXPECT_TRUE(two->firstChild()->CanStartSelection());
}
TEST_F(NodeTest, canStartSelectionWithShadowDOM) {
const char* body_content = "<div id=host><span id=one>one</span></div>";
const char* shadow_content =
"<a href='http://www.msn.com'><content></content></a>";
SetBodyContent(body_content);
SetShadowContent(shadow_content, "host");
Node* one = GetDocument().getElementById("one");
EXPECT_FALSE(one->CanStartSelection());
EXPECT_FALSE(one->firstChild()->CanStartSelection());
}
TEST_F(NodeTest, customElementState) {
const char* body_content = "<div id=div></div>";
SetBodyContent(body_content);
Element* div = GetDocument().getElementById("div");
EXPECT_EQ(CustomElementState::kUncustomized, div->GetCustomElementState());
EXPECT_TRUE(div->IsDefined());
EXPECT_EQ(Node::kV0NotCustomElement, div->GetV0CustomElementState());
div->SetCustomElementState(CustomElementState::kUndefined);
EXPECT_EQ(CustomElementState::kUndefined, div->GetCustomElementState());
EXPECT_FALSE(div->IsDefined());
EXPECT_EQ(Node::kV0NotCustomElement, div->GetV0CustomElementState());
div->SetCustomElementState(CustomElementState::kCustom);
EXPECT_EQ(CustomElementState::kCustom, div->GetCustomElementState());
EXPECT_TRUE(div->IsDefined());
EXPECT_EQ(Node::kV0NotCustomElement, div->GetV0CustomElementState());
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_TextRoot) {
SetBodyContent("Text");
Node* root = GetDocument().body()->firstChild();
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_InlineRoot) {
SetBodyContent("<span id=root>Text <span></span></span>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_BlockRoot) {
SetBodyContent("<div id=root>Text <span></span></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_FloatRoot) {
SetBodyContent("<div id=root style='float:left'><span></span></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_FALSE(previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_AbsoluteRoot) {
SetBodyContent("<div id=root style='position:absolute'><span></span></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_FALSE(previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_Text) {
SetBodyContent("<div id=root style='display:contents'>Text</div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->firstChild()->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_Inline) {
SetBodyContent("<div id=root style='display:contents'><span></span></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->firstChild()->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_Block) {
SetBodyContent("<div id=root style='display:contents'><div></div></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(root->firstChild()->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_Float) {
SetBodyContent(
"<style>"
" #root { display:contents }"
" .float { float:left }"
"</style>"
"<div id=root><div class=float></div></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_FALSE(previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_AbsolutePositioned) {
SetBodyContent(
"<style>"
" #root { display:contents }"
" .abs { position:absolute }"
"</style>"
"<div id=root><div class=abs></div></div>");
Element* root = GetDocument().getElementById("root");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_FALSE(previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_SkipAbsolute) {
SetBodyContent(
"<style>"
" #root { display:contents }"
" .abs { position:absolute }"
"</style>"
"<div id=root>"
"<div class=abs></div><span id=inline></span><div class=abs></div>"
"</div>");
Element* root = GetDocument().getElementById("root");
Element* span = GetDocument().getElementById("inline");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(span->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_SkipFloats) {
SetBodyContent(
"<style>"
" #root { display:contents }"
" .float { float:left }"
"</style>"
"<div id=root>"
"<div class=float></div>"
"<span id=inline></span>"
"<div class=float></div>"
"</div>");
Element* root = GetDocument().getElementById("root");
Element* span = GetDocument().getElementById("inline");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(span->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_InsideDisplayContents) {
SetBodyContent(
"<style>"
" #root, .contents { display:contents }"
" .float { float:left }"
"</style>"
"<div id=root>"
"<span></span><div class=contents><span id=inline></span></div>"
"</div>");
Element* root = GetDocument().getElementById("root");
Element* span = GetDocument().getElementById("inline");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(span->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_Slotted) {
SetBodyContent("<div id=host><span id=inline></span></div>");
ShadowRoot& shadow_root =
GetDocument().getElementById("host")->AttachShadowRootInternal(
ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString(
"<div id=root style='display:contents'><span></span><slot></slot></div>");
UpdateAllLifecyclePhasesForTest();
Element* root = shadow_root.getElementById("root");
Element* span = GetDocument().getElementById("inline");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(span->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, AttachContext_PreviousInFlow_V0Content) {
SetBodyContent("<div id=host><span id=inline></span></div>");
ShadowRoot* shadow_root = CreateShadowRootForElementWithIDAndSetInnerHTML(
GetDocument(), "host",
"<div id=root style='display:contents'><span></span><content /></div>");
Element* root = shadow_root->getElementById("root");
Element* span = GetDocument().getElementById("inline");
LayoutObject* previous_in_flow = ReattachLayoutTreeForNode(*root);
EXPECT_TRUE(previous_in_flow);
EXPECT_EQ(span->GetLayoutObject(), previous_in_flow);
}
TEST_F(NodeTest, HasMediaControlAncestor_Fail) {
auto* node = MakeGarbageCollected<HTMLDivElement>(GetDocument());
EXPECT_FALSE(node->HasMediaControlAncestor());
EXPECT_FALSE(InitializeUserAgentShadowTree(node)->HasMediaControlAncestor());
}
TEST_F(NodeTest, HasMediaControlAncestor_MediaControlElement) {
FakeMediaControlElement* node =
MakeGarbageCollected<FakeMediaControlElement>(GetDocument());
EXPECT_TRUE(node->HasMediaControlAncestor());
EXPECT_TRUE(InitializeUserAgentShadowTree(node)->HasMediaControlAncestor());
}
TEST_F(NodeTest, HasMediaControlAncestor_MediaControls) {
FakeMediaControls* node =
MakeGarbageCollected<FakeMediaControls>(GetDocument());
EXPECT_TRUE(node->HasMediaControlAncestor());
EXPECT_TRUE(InitializeUserAgentShadowTree(node)->HasMediaControlAncestor());
}
TEST_F(NodeTest, appendChildProcessingInstructionNoStyleRecalc) {
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(GetDocument().ChildNeedsStyleRecalc());
auto* pi =
MakeGarbageCollected<ProcessingInstruction>(GetDocument(), "A", "B");
GetDocument().body()->appendChild(pi, ASSERT_NO_EXCEPTION);
EXPECT_FALSE(GetDocument().ChildNeedsStyleRecalc());
}
TEST_F(NodeTest, appendChildCommentNoStyleRecalc) {
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(GetDocument().ChildNeedsStyleRecalc());
Comment* comment = Comment::Create(GetDocument(), "comment");
GetDocument().body()->appendChild(comment, ASSERT_NO_EXCEPTION);
EXPECT_FALSE(GetDocument().ChildNeedsStyleRecalc());
}
TEST_F(NodeTest, MutationOutsideFlatTreeStyleDirty) {
SetBodyContent("<div id=host><span id=nonslotted></span></div>");
GetDocument().getElementById("host")->AttachShadowRootInternal(
ShadowRootType::kOpen);
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(GetDocument().NeedsLayoutTreeUpdate());
GetDocument()
.getElementById("nonslotted")
->setAttribute("style", "color:green");
EXPECT_EQ(!RuntimeEnabledFeatures::FlatTreeStyleRecalcEnabled(),
GetDocument().NeedsLayoutTreeUpdate());
}
TEST_F(NodeTest, SkipStyleDirtyHostChild) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span></div>");
Element* host = GetDocument().getElementById("host");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString(
"<div style='display:none'><slot></slot></div>");
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(GetDocument().NeedsLayoutTreeUpdate());
// Check that we do not mark an element for style recalc when the element and
// its flat tree parent are display:none.
To<Element>(host->firstChild())->setAttribute("style", "color:green");
EXPECT_FALSE(GetDocument().NeedsLayoutTreeUpdate());
}
TEST_F(NodeTest, ContainsChild) {
SetBodyContent("<div id=a><div id=b></div></div>");
Element* a = GetDocument().getElementById("a");
Element* b = GetDocument().getElementById("b");
EXPECT_TRUE(a->contains(b));
}
TEST_F(NodeTest, ContainsNoSibling) {
SetBodyContent("<div id=a></div><div id=b></div>");
Element* a = GetDocument().getElementById("a");
Element* b = GetDocument().getElementById("b");
EXPECT_FALSE(a->contains(b));
}
TEST_F(NodeTest, ContainsPseudo) {
SetBodyContent(
"<style>#a::before{content:'aaa';}</style>"
"<div id=a></div>");
Element* a = GetDocument().getElementById("a");
PseudoElement* pseudo = a->GetPseudoElement(kPseudoIdBefore);
ASSERT_TRUE(pseudo);
EXPECT_TRUE(a->contains(pseudo));
}
TEST_F(NodeTest, SkipForceReattachDisplayNone) {
SetBodyContent("<div id=host><span style='display:none'></span></div>");
Element* host = GetDocument().getElementById("host");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString("<slot name='target'></slot>");
UpdateAllLifecyclePhasesForTest();
Element* span = To<Element>(host->firstChild());
span->setAttribute(html_names::kSlotAttr, "target");
GetDocument().GetSlotAssignmentEngine().RecalcSlotAssignments();
// Node::FlatTreeParentChanged for a display:none could trigger style recalc,
// but we should skip a forced re-attach for nodes with a null ComputedStyle.
EXPECT_TRUE(GetDocument().NeedsLayoutTreeUpdate());
EXPECT_TRUE(span->NeedsStyleRecalc());
EXPECT_FALSE(span->GetForceReattachLayoutTree());
}
TEST_F(NodeTest, UpdateChildDirtyAncestorsOnSlotAssignment) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span></div>");
Element* host = GetDocument().getElementById("host");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString(
"<div><slot></slot></div><div id='child-dirty'><slot "
"name='target'></slot></div>");
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(GetDocument().NeedsLayoutTreeUpdate());
auto* span = To<Element>(host->firstChild());
auto* ancestor = shadow_root.getElementById("child-dirty");
// Make sure the span is dirty before the re-assignment.
span->setAttribute("style", "color:green");
EXPECT_FALSE(ancestor->ChildNeedsStyleRecalc());
// Re-assign to second slot.
span->setAttribute(html_names::kSlotAttr, "target");
GetDocument().GetSlotAssignmentEngine().RecalcSlotAssignments();
EXPECT_TRUE(ancestor->ChildNeedsStyleRecalc());
}
TEST_F(NodeTest, UpdateChildDirtySlotAfterRemoval) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span></div>");
Element* host = GetDocument().getElementById("host");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString("<slot></slot>");
UpdateAllLifecyclePhasesForTest();
auto* span = To<Element>(host->firstChild());
auto* slot = shadow_root.firstChild();
// Make sure the span is dirty, and the slot marked child-dirty before the
// removal.
span->setAttribute("style", "color:green");
EXPECT_TRUE(span->NeedsStyleRecalc());
EXPECT_TRUE(slot->ChildNeedsStyleRecalc());
EXPECT_TRUE(host->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().body()->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().GetStyleEngine().NeedsStyleRecalc());
// The StyleRecalcRoot is now the span. Removing the span should clear the
// root and the child-dirty bits on the ancestors.
span->remove();
EXPECT_FALSE(slot->ChildNeedsStyleRecalc());
EXPECT_FALSE(host->ChildNeedsStyleRecalc());
EXPECT_FALSE(GetDocument().body()->ChildNeedsStyleRecalc());
EXPECT_FALSE(GetDocument().GetStyleEngine().NeedsStyleRecalc());
}
TEST_F(NodeTest, UpdateChildDirtyAfterSlotRemoval) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span></div>");
Element* host = GetDocument().getElementById("host");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString("<div><slot></slot></div>");
UpdateAllLifecyclePhasesForTest();
auto* span = To<Element>(host->firstChild());
auto* div = shadow_root.firstChild();
auto* slot = div->firstChild();
// Make sure the span is dirty, and the slot marked child-dirty before the
// removal.
span->setAttribute("style", "color:green");
EXPECT_TRUE(span->NeedsStyleRecalc());
EXPECT_TRUE(slot->ChildNeedsStyleRecalc());
EXPECT_TRUE(div->ChildNeedsStyleRecalc());
EXPECT_TRUE(host->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().body()->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().GetStyleEngine().NeedsStyleRecalc());
// The StyleRecalcRoot is now the span. Removing the slot would break the flat
// tree ancestor chain so that when removing the span we would no longer be
// able to clear the dirty bits for all of the previous ancestor chain. Thus,
// we fall back to use the host as the style recalc root to be able to
// traverse and clear the dirty bit of the shadow tree div element on the next
// style recalc.
slot->remove();
span->remove();
EXPECT_TRUE(div->ChildNeedsStyleRecalc());
EXPECT_TRUE(host->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().body()->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().GetStyleEngine().NeedsStyleRecalc());
}
TEST_F(NodeTest, UpdateChildDirtyAfterSlottingDirtyNode) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span></div>");
auto* host = GetDocument().getElementById("host");
auto* span = To<Element>(host->firstChild());
// Make sure the span is style dirty.
span->setAttribute("style", "color:green");
ShadowRoot& shadow_root =
host->AttachShadowRootInternal(ShadowRootType::kOpen);
shadow_root.SetInnerHTMLFromString("<div><slot></slot></div>");
GetDocument().GetSlotAssignmentEngine().RecalcSlotAssignments();
// Make sure shadow tree div and slot are marked with ChildNeedsStyleRecalc
// when the dirty span is slotted in.
EXPECT_TRUE(shadow_root.firstChild()->ChildNeedsStyleRecalc());
EXPECT_TRUE(shadow_root.firstChild()->firstChild()->ChildNeedsStyleRecalc());
EXPECT_TRUE(span->NeedsStyleRecalc());
}
TEST_F(NodeTest, ChildDirtyNeedsV0Distribution) {
ScopedFlatTreeStyleRecalcForTest scope(true);
SetBodyContent("<div id=host><span></span> </div>");
ShadowRoot* shadow_root = CreateShadowRootForElementWithIDAndSetInnerHTML(
GetDocument(), "host", "<content />");
UpdateAllLifecyclePhasesForTest();
#if DCHECK_IS_ON()
GetDocument().SetAllowDirtyShadowV0Traversal(true);
#endif
auto* host = GetDocument().getElementById("host");
auto* span = To<Element>(host->firstChild());
auto* content = shadow_root->firstChild();
host->lastChild()->remove();
EXPECT_FALSE(GetDocument().documentElement()->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().documentElement()->ChildNeedsDistributionRecalc());
EXPECT_EQ(content, host->firstChild()->GetStyleRecalcParent());
EXPECT_FALSE(content->ChildNeedsStyleRecalc());
// Make the span style dirty.
span->setAttribute("style", "color:green");
// Check that the flat tree ancestor chain is child-dirty while the
// shadow distribution is still dirty.
EXPECT_TRUE(GetDocument().documentElement()->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().body()->ChildNeedsStyleRecalc());
EXPECT_TRUE(host->ChildNeedsStyleRecalc());
EXPECT_TRUE(content->ChildNeedsStyleRecalc());
EXPECT_TRUE(GetDocument().documentElement()->ChildNeedsDistributionRecalc());
#if DCHECK_IS_ON()
GetDocument().SetAllowDirtyShadowV0Traversal(false);
#endif
}
} // namespace blink