blob: 521bc13e1f299dfaa0efe5155b5fc27f35346628 [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/dom/element.h"
#include <memory>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/web/web_plugin.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_token_list.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/editing/testing/editing_test_base.h"
#include "third_party/blink/renderer/core/exported/web_plugin_container_impl.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/geometry/dom_rect.h"
#include "third_party/blink/renderer/core/html/html_html_element.h"
#include "third_party/blink/renderer/core/html/html_plugin_element.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h"
namespace blink {
class ElementTest : public EditingTestBase {};
TEST_F(ElementTest, SupportsFocus) {
Document& document = GetDocument();
DCHECK(IsA<HTMLHtmlElement>(document.documentElement()));
document.setDesignMode("on");
document.View()->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest);
EXPECT_TRUE(document.documentElement()->SupportsFocus())
<< "<html> with designMode=on should be focusable.";
}
TEST_F(ElementTest,
GetBoundingClientRectCorrectForStickyElementsAfterInsertion) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<style>body { margin: 0 }
#scroller { overflow: scroll; height: 100px; width: 100px; }
#sticky { height: 25px; position: sticky; top: 0; left: 25px; }
#padding { height: 500px; width: 300px; }</style>
<div id='scroller'><div id='writer'></div><div id='sticky'></div>
<div id='padding'></div></div>
)HTML");
Element* scroller = document.getElementById("scroller");
Element* writer = document.getElementById("writer");
Element* sticky = document.getElementById("sticky");
ASSERT_TRUE(scroller);
ASSERT_TRUE(writer);
ASSERT_TRUE(sticky);
scroller->scrollTo(50.0, 200.0);
// The sticky element should remain at (0, 25) relative to the viewport due to
// the constraints.
DOMRect* bounding_client_rect = sticky->getBoundingClientRect();
EXPECT_EQ(0, bounding_client_rect->top());
EXPECT_EQ(25, bounding_client_rect->left());
// Insert a new <div> above the sticky. This will dirty layout and invalidate
// the sticky constraints.
writer->setInnerHTML("<div style='height: 100px; width: 700px;'></div>");
EXPECT_EQ(DocumentLifecycle::kVisualUpdatePending,
document.Lifecycle().GetState());
// Requesting the bounding client rect should cause both layout and
// compositing inputs clean to be run, and the sticky result shouldn't change.
bounding_client_rect = sticky->getBoundingClientRect();
EXPECT_EQ(DocumentLifecycle::kCompositingInputsClean,
document.Lifecycle().GetState());
EXPECT_FALSE(sticky->GetLayoutBoxModelObject()
->Layer()
->NeedsCompositingInputsUpdate());
EXPECT_EQ(0, bounding_client_rect->top());
EXPECT_EQ(25, bounding_client_rect->left());
}
TEST_F(ElementTest, OffsetTopAndLeftCorrectForStickyElementsAfterInsertion) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<style>body { margin: 0 }
#scroller { overflow: scroll; height: 100px; width: 100px; }
#sticky { height: 25px; position: sticky; top: 0; left: 25px; }
#padding { height: 500px; width: 300px; }</style>
<div id='scroller'><div id='writer'></div><div id='sticky'></div>
<div id='padding'></div></div>
)HTML");
Element* scroller = document.getElementById("scroller");
Element* writer = document.getElementById("writer");
Element* sticky = document.getElementById("sticky");
ASSERT_TRUE(scroller);
ASSERT_TRUE(writer);
ASSERT_TRUE(sticky);
scroller->scrollTo(50.0, 200.0);
// The sticky element should be offset to stay at (0, 25) relative to the
// viewport due to the constraints.
EXPECT_EQ(scroller->scrollTop(), sticky->OffsetTop());
EXPECT_EQ(scroller->scrollLeft() + 25, sticky->OffsetLeft());
// Insert a new <div> above the sticky. This will dirty layout and invalidate
// the sticky constraints.
writer->setInnerHTML("<div style='height: 100px; width: 700px;'></div>");
EXPECT_EQ(DocumentLifecycle::kVisualUpdatePending,
document.Lifecycle().GetState());
// Requesting either offset should cause both layout and compositing inputs
// clean to be run, and the sticky result shouldn't change.
EXPECT_EQ(scroller->scrollTop(), sticky->OffsetTop());
EXPECT_EQ(DocumentLifecycle::kCompositingInputsClean,
document.Lifecycle().GetState());
EXPECT_FALSE(sticky->GetLayoutBoxModelObject()
->Layer()
->NeedsCompositingInputsUpdate());
// Dirty layout again, since |OffsetTop| will have cleaned it.
writer->setInnerHTML("<div style='height: 100px; width: 700px;'></div>");
EXPECT_EQ(DocumentLifecycle::kVisualUpdatePending,
document.Lifecycle().GetState());
// Again requesting an offset should cause layout and compositing to be clean.
EXPECT_EQ(scroller->scrollLeft() + 25, sticky->OffsetLeft());
EXPECT_EQ(DocumentLifecycle::kCompositingInputsClean,
document.Lifecycle().GetState());
EXPECT_FALSE(sticky->GetLayoutBoxModelObject()
->Layer()
->NeedsCompositingInputsUpdate());
}
TEST_F(ElementTest, BoundsInViewportCorrectForStickyElementsAfterInsertion) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<style>body { margin: 0 }
#scroller { overflow: scroll; height: 100px; width: 100px; }
#sticky { height: 25px; position: sticky; top: 0; left: 25px; }
#padding { height: 500px; width: 300px; }</style>
<div id='scroller'><div id='writer'></div><div id='sticky'></div>
<div id='padding'></div></div>
)HTML");
Element* scroller = document.getElementById("scroller");
Element* writer = document.getElementById("writer");
Element* sticky = document.getElementById("sticky");
ASSERT_TRUE(scroller);
ASSERT_TRUE(writer);
ASSERT_TRUE(sticky);
scroller->scrollTo(50.0, 200.0);
// The sticky element should remain at (0, 25) relative to the viewport due to
// the constraints.
IntRect bounds_in_viewport = sticky->BoundsInViewport();
EXPECT_EQ(0, bounds_in_viewport.Y());
EXPECT_EQ(25, bounds_in_viewport.X());
// Insert a new <div> above the sticky. This will dirty layout and invalidate
// the sticky constraints.
writer->setInnerHTML("<div style='height: 100px; width: 700px;'></div>");
EXPECT_EQ(DocumentLifecycle::kVisualUpdatePending,
document.Lifecycle().GetState());
// Requesting the bounds in viewport should cause both layout and compositing
// inputs clean to be run, and the sticky result shouldn't change.
bounds_in_viewport = sticky->BoundsInViewport();
EXPECT_EQ(DocumentLifecycle::kCompositingInputsClean,
document.Lifecycle().GetState());
EXPECT_FALSE(sticky->GetLayoutBoxModelObject()
->Layer()
->NeedsCompositingInputsUpdate());
EXPECT_EQ(0, bounds_in_viewport.Y());
EXPECT_EQ(25, bounds_in_viewport.X());
}
TEST_F(ElementTest, OutlineRectsIncludesImgChildren) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<a id='link' href=''><img id='image' width='220' height='147'></a>
)HTML");
Element* a = document.getElementById("link");
Element* img = document.getElementById("image");
ASSERT_TRUE(a);
ASSERT_TRUE(img);
// The a element should include the image in computing its bounds.
IntRect img_bounds_in_viewport = img->BoundsInViewport();
EXPECT_EQ(220, img_bounds_in_viewport.Width());
EXPECT_EQ(147, img_bounds_in_viewport.Height());
LOG(INFO) << "img_bounds_in_viewport: " << img_bounds_in_viewport;
Vector<IntRect> a_outline_rects = a->OutlineRectsInVisualViewport();
EXPECT_EQ(2u, a_outline_rects.size());
IntRect a_outline_rect;
for (auto& r : a_outline_rects) {
a_outline_rect.Unite(r);
LOG(INFO) << "r: " << r;
LOG(INFO) << "a_outline_rect: " << a_outline_rect;
}
EXPECT_EQ(img_bounds_in_viewport.Width(), a_outline_rect.Width());
EXPECT_EQ(img_bounds_in_viewport.Height(), a_outline_rect.Height());
}
TEST_F(ElementTest, StickySubtreesAreTrackedCorrectly) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<div id='ancestor'>
<div id='outerSticky' style='position:sticky;'>
<div id='child'>
<div id='grandchild'></div>
<div id='innerSticky' style='position:sticky;'>
<div id='greatGrandchild'></div>
</div>
</div
</div>
</div>
)HTML");
LayoutObject* ancestor =
document.getElementById("ancestor")->GetLayoutObject();
LayoutObject* outer_sticky =
document.getElementById("outerSticky")->GetLayoutObject();
LayoutObject* child = document.getElementById("child")->GetLayoutObject();
LayoutObject* grandchild =
document.getElementById("grandchild")->GetLayoutObject();
LayoutObject* inner_sticky =
document.getElementById("innerSticky")->GetLayoutObject();
LayoutObject* great_grandchild =
document.getElementById("greatGrandchild")->GetLayoutObject();
EXPECT_FALSE(ancestor->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(outer_sticky->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(child->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(grandchild->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(inner_sticky->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(great_grandchild->StyleRef().SubtreeIsSticky());
// This forces 'child' to fork it's StyleRareInheritedData, so that we can
// ensure that the sticky subtree update behavior survives forking.
document.getElementById("child")->SetInlineStyleProperty(
CSSPropertyID::kWebkitRubyPosition, CSSValueID::kAfter);
document.View()->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kPaintClean, document.Lifecycle().GetState());
EXPECT_EQ(RubyPosition::kBefore, outer_sticky->StyleRef().GetRubyPosition());
EXPECT_EQ(RubyPosition::kAfter, child->StyleRef().GetRubyPosition());
EXPECT_EQ(RubyPosition::kAfter, grandchild->StyleRef().GetRubyPosition());
EXPECT_EQ(RubyPosition::kAfter, inner_sticky->StyleRef().GetRubyPosition());
EXPECT_EQ(RubyPosition::kAfter,
great_grandchild->StyleRef().GetRubyPosition());
// Setting -webkit-ruby value shouldn't have affected the sticky subtree bit.
EXPECT_TRUE(outer_sticky->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(child->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(grandchild->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(inner_sticky->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(great_grandchild->StyleRef().SubtreeIsSticky());
// Now switch 'outerSticky' back to being non-sticky - all descendents between
// it and the 'innerSticky' should be updated, and the 'innerSticky' should
// fork it's StyleRareInheritedData to maintain the sticky subtree bit.
document.getElementById("outerSticky")
->SetInlineStyleProperty(CSSPropertyID::kPosition, CSSValueID::kStatic);
document.View()->UpdateAllLifecyclePhases(DocumentUpdateReason::kTest);
EXPECT_EQ(DocumentLifecycle::kPaintClean, document.Lifecycle().GetState());
EXPECT_FALSE(outer_sticky->StyleRef().SubtreeIsSticky());
EXPECT_FALSE(child->StyleRef().SubtreeIsSticky());
EXPECT_FALSE(grandchild->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(inner_sticky->StyleRef().SubtreeIsSticky());
EXPECT_TRUE(great_grandchild->StyleRef().SubtreeIsSticky());
}
TEST_F(ElementTest, GetElementsByClassNameCrash) {
// Test for a crash in NodeListsNodeData::AddCache().
GetDocument().SetCompatibilityMode(Document::kQuirksMode);
ASSERT_TRUE(GetDocument().InQuirksMode());
GetDocument().body()->getElementsByClassName("ABC DEF");
GetDocument().body()->getElementsByClassName("ABC DEF");
// The test passes if no crash happens.
}
TEST_F(ElementTest, GetBoundingClientRectForSVG) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<style>body { margin: 0 }</style>
<svg width='500' height='500'>
<rect id='rect' x='10' y='100' width='100' height='71'/>
<rect id='stroke' x='10' y='100' width='100' height='71'
stroke-width='7'/>
<rect id='stroke_transformed' x='10' y='100' width='100' height='71'
stroke-width='7' transform='translate(3, 5)'/>
<foreignObject id='foreign' x='10' y='100' width='100' height='71'/>
<foreignObject id='foreign_transformed' transform='translate(3, 5)'
x='10' y='100' width='100' height='71'/>
<svg id='svg' x='10' y='100'>
<rect width='100' height='71'/>
</svg>
<svg id='svg_stroke' x='10' y='100'>
<rect width='100' height='71' stroke-width='7'/>
</svg>
</svg>
)HTML");
Element* rect = document.getElementById("rect");
DOMRect* rect_bounding_client_rect = rect->getBoundingClientRect();
EXPECT_EQ(10, rect_bounding_client_rect->left());
EXPECT_EQ(100, rect_bounding_client_rect->top());
EXPECT_EQ(100, rect_bounding_client_rect->width());
EXPECT_EQ(71, rect_bounding_client_rect->height());
EXPECT_EQ(IntRect(10, 100, 100, 71), rect->BoundsInViewport());
// TODO(pdr): Should we should be excluding the stroke (here, and below)?
// See: https://github.com/w3c/svgwg/issues/339 and Element::ClientQuads.
Element* stroke = document.getElementById("stroke");
DOMRect* stroke_bounding_client_rect = stroke->getBoundingClientRect();
EXPECT_EQ(10, stroke_bounding_client_rect->left());
EXPECT_EQ(100, stroke_bounding_client_rect->top());
EXPECT_EQ(100, stroke_bounding_client_rect->width());
EXPECT_EQ(71, stroke_bounding_client_rect->height());
// TODO(pdr): BoundsInViewport is not web exposed and should include stroke.
EXPECT_EQ(IntRect(10, 100, 100, 71), stroke->BoundsInViewport());
Element* stroke_transformed = document.getElementById("stroke_transformed");
DOMRect* stroke_transformedbounding_client_rect =
stroke_transformed->getBoundingClientRect();
EXPECT_EQ(13, stroke_transformedbounding_client_rect->left());
EXPECT_EQ(105, stroke_transformedbounding_client_rect->top());
EXPECT_EQ(100, stroke_transformedbounding_client_rect->width());
EXPECT_EQ(71, stroke_transformedbounding_client_rect->height());
// TODO(pdr): BoundsInViewport is not web exposed and should include stroke.
EXPECT_EQ(IntRect(13, 105, 100, 71), stroke_transformed->BoundsInViewport());
Element* foreign = document.getElementById("foreign");
DOMRect* foreign_bounding_client_rect = foreign->getBoundingClientRect();
EXPECT_EQ(10, foreign_bounding_client_rect->left());
EXPECT_EQ(100, foreign_bounding_client_rect->top());
EXPECT_EQ(100, foreign_bounding_client_rect->width());
EXPECT_EQ(71, foreign_bounding_client_rect->height());
EXPECT_EQ(IntRect(10, 100, 100, 71), foreign->BoundsInViewport());
Element* foreign_transformed = document.getElementById("foreign_transformed");
DOMRect* foreign_transformed_bounding_client_rect =
foreign_transformed->getBoundingClientRect();
EXPECT_EQ(13, foreign_transformed_bounding_client_rect->left());
EXPECT_EQ(105, foreign_transformed_bounding_client_rect->top());
EXPECT_EQ(100, foreign_transformed_bounding_client_rect->width());
EXPECT_EQ(71, foreign_transformed_bounding_client_rect->height());
EXPECT_EQ(IntRect(13, 105, 100, 71), foreign_transformed->BoundsInViewport());
Element* svg = document.getElementById("svg");
DOMRect* svg_bounding_client_rect = svg->getBoundingClientRect();
EXPECT_EQ(10, svg_bounding_client_rect->left());
EXPECT_EQ(100, svg_bounding_client_rect->top());
EXPECT_EQ(100, svg_bounding_client_rect->width());
EXPECT_EQ(71, svg_bounding_client_rect->height());
EXPECT_EQ(IntRect(10, 100, 100, 71), svg->BoundsInViewport());
Element* svg_stroke = document.getElementById("svg_stroke");
DOMRect* svg_stroke_bounding_client_rect =
svg_stroke->getBoundingClientRect();
EXPECT_EQ(10, svg_stroke_bounding_client_rect->left());
EXPECT_EQ(100, svg_stroke_bounding_client_rect->top());
EXPECT_EQ(100, svg_stroke_bounding_client_rect->width());
EXPECT_EQ(71, svg_stroke_bounding_client_rect->height());
// TODO(pdr): BoundsInViewport is not web exposed and should include stroke.
EXPECT_EQ(IntRect(10, 100, 100, 71), svg_stroke->BoundsInViewport());
}
TEST_F(ElementTest, PartAttribute) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<span id='has_one_part' part='partname'></span>
<span id='has_two_parts' part='partname1 partname2'></span>
<span id='has_no_part'></span>
)HTML");
Element* has_one_part = document.getElementById("has_one_part");
Element* has_two_parts = document.getElementById("has_two_parts");
Element* has_no_part = document.getElementById("has_no_part");
ASSERT_TRUE(has_no_part);
ASSERT_TRUE(has_one_part);
ASSERT_TRUE(has_two_parts);
{
EXPECT_TRUE(has_one_part->HasPart());
const DOMTokenList* part = has_one_part->GetPart();
ASSERT_TRUE(part);
ASSERT_EQ(1UL, part->length());
ASSERT_EQ("partname", part->value());
}
{
EXPECT_TRUE(has_two_parts->HasPart());
const DOMTokenList* part = has_two_parts->GetPart();
ASSERT_TRUE(part);
ASSERT_EQ(2UL, part->length());
ASSERT_EQ("partname1 partname2", part->value());
}
{
EXPECT_FALSE(has_no_part->HasPart());
EXPECT_FALSE(has_no_part->GetPart());
// Calling the DOM API should force creation of an empty DOMTokenList.
const DOMTokenList& part = has_no_part->part();
EXPECT_FALSE(has_no_part->HasPart());
EXPECT_EQ(&part, has_no_part->GetPart());
// Now update the attribute value and make sure it's reflected.
has_no_part->setAttribute("part", "partname");
ASSERT_EQ(1UL, part.length());
ASSERT_EQ("partname", part.value());
}
}
TEST_F(ElementTest, ExportpartsAttribute) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<span id='has_one_mapping' exportparts='partname1: partname2'></span>
<span id='has_two_mappings' exportparts='partname1: partname2, partname3: partname4'></span>
<span id='has_no_mapping'></span>
)HTML");
Element* has_one_mapping = document.getElementById("has_one_mapping");
Element* has_two_mappings = document.getElementById("has_two_mappings");
Element* has_no_mapping = document.getElementById("has_no_mapping");
ASSERT_TRUE(has_no_mapping);
ASSERT_TRUE(has_one_mapping);
ASSERT_TRUE(has_two_mappings);
{
EXPECT_TRUE(has_one_mapping->HasPartNamesMap());
const NamesMap* part_names_map = has_one_mapping->PartNamesMap();
ASSERT_TRUE(part_names_map);
ASSERT_EQ(1UL, part_names_map->size());
ASSERT_EQ("partname2",
part_names_map->Get("partname1").value().SerializeToString());
}
{
EXPECT_TRUE(has_two_mappings->HasPartNamesMap());
const NamesMap* part_names_map = has_two_mappings->PartNamesMap();
ASSERT_TRUE(part_names_map);
ASSERT_EQ(2UL, part_names_map->size());
ASSERT_EQ("partname2",
part_names_map->Get("partname1").value().SerializeToString());
ASSERT_EQ("partname4",
part_names_map->Get("partname3").value().SerializeToString());
}
{
EXPECT_FALSE(has_no_mapping->HasPartNamesMap());
EXPECT_FALSE(has_no_mapping->PartNamesMap());
// Now update the attribute value and make sure it's reflected.
has_no_mapping->setAttribute("exportparts", "partname1: partname2");
const NamesMap* part_names_map = has_no_mapping->PartNamesMap();
ASSERT_TRUE(part_names_map);
ASSERT_EQ(1UL, part_names_map->size());
ASSERT_EQ("partname2",
part_names_map->Get("partname1").value().SerializeToString());
}
}
TEST_F(ElementTest, OptionElementDisplayNoneComputedStyle) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<optgroup id=group style='display:none'></optgroup>
<option id=option style='display:none'></option>
<div style='display:none'>
<optgroup id=inner-group></optgroup>
<option id=inner-option></option>
</div>
)HTML");
EXPECT_FALSE(document.getElementById("group")->GetComputedStyle());
EXPECT_FALSE(document.getElementById("option")->GetComputedStyle());
EXPECT_FALSE(document.getElementById("inner-group")->GetComputedStyle());
EXPECT_FALSE(document.getElementById("inner-option")->GetComputedStyle());
}
// A fake plugin which will assert that script is allowed in Destroy.
class ScriptOnDestroyPlugin : public GarbageCollected<ScriptOnDestroyPlugin>,
public WebPlugin {
public:
bool Initialize(WebPluginContainer* container) override {
container_ = container;
return true;
}
void Destroy() override {
destroy_called_ = true;
ASSERT_FALSE(ScriptForbiddenScope::IsScriptForbidden());
}
WebPluginContainer* Container() const override { return container_; }
void UpdateAllLifecyclePhases(DocumentUpdateReason) override {}
void Paint(cc::PaintCanvas*, const WebRect&) override {}
void UpdateGeometry(const WebRect&,
const WebRect&,
const WebRect&,
bool) override {}
void UpdateFocus(bool, mojom::blink::FocusType) override {}
void UpdateVisibility(bool) override {}
WebInputEventResult HandleInputEvent(const WebCoalescedInputEvent&,
ui::Cursor*) override {
return {};
}
void DidReceiveResponse(const WebURLResponse&) override {}
void DidReceiveData(const char* data, size_t data_length) override {}
void DidFinishLoading() override {}
void DidFailLoading(const WebURLError&) override {}
void Trace(Visitor*) const {}
bool DestroyCalled() const { return destroy_called_; }
private:
WebPluginContainer* container_;
bool destroy_called_ = false;
};
TEST_F(ElementTest, CreateAndAttachShadowRootSuspendsPluginDisposal) {
Document& document = GetDocument();
SetBodyContent(R"HTML(
<div id=target>
<embed id=plugin type=application/x-blink-text-plugin></embed>
</div>
)HTML");
// Set the plugin element up to have the ScriptOnDestroy plugin.
auto* plugin_element =
DynamicTo<HTMLPlugInElement>(document.getElementById("plugin"));
ASSERT_TRUE(plugin_element);
auto* plugin = MakeGarbageCollected<ScriptOnDestroyPlugin>();
auto* plugin_container =
MakeGarbageCollected<WebPluginContainerImpl>(*plugin_element, plugin);
plugin->Initialize(plugin_container);
plugin_element->SetEmbeddedContentView(plugin_container);
// Now create a shadow root on target, which should cause the plugin to be
// destroyed. Test passes if we pass the script forbidden check in the plugin.
auto* target = document.getElementById("target");
target->CreateUserAgentShadowRoot();
ASSERT_TRUE(plugin->DestroyCalled());
}
} // namespace blink