blob: defad7588d2f02fa5d3ceb69cd681781d3c529f3 [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 "core/dom/custom/CustomElementRegistry.h"
#include "bindings/core/v8/ExceptionState.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/ElementDefinitionOptions.h"
#include "core/dom/custom/CEReactionsScope.h"
#include "core/dom/custom/CustomElementDefinition.h"
#include "core/dom/custom/CustomElementDefinitionBuilder.h"
#include "core/dom/custom/CustomElementDescriptor.h"
#include "core/dom/custom/CustomElementTestHelpers.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/dom/shadow/ShadowRootInit.h"
#include "core/testing/DummyPageHolder.h"
#include "platform/ScriptForbiddenScope.h"
#include "platform/heap/Handle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "wtf/text/AtomicString.h"
#include <memory>
namespace blink {
class CustomElementRegistryTest : public ::testing::Test {
protected:
void SetUp() {
m_page.reset(DummyPageHolder::create(IntSize(1, 1)).release());
}
void TearDown() { m_page = nullptr; }
Document& document() { return m_page->document(); }
CustomElementRegistry& registry() {
return *m_page->frame().localDOMWindow()->customElements();
}
ScriptState* scriptState() {
return ScriptState::forMainWorld(&m_page->frame());
}
void collectCandidates(const CustomElementDescriptor& desc,
HeapVector<Member<Element>>* elements) {
registry().collectCandidates(desc, elements);
}
ShadowRoot* attachShadowTo(Element* element) {
NonThrowableExceptionState noExceptions;
ShadowRootInit shadowRootInit;
return element->attachShadow(scriptState(), shadowRootInit, noExceptions);
}
private:
std::unique_ptr<DummyPageHolder> m_page;
};
TEST_F(CustomElementRegistryTest,
collectCandidates_shouldNotIncludeElementsRemovedFromDocument) {
Element* element = CreateElement("a-a").inDocument(&document());
registry().addCandidate(element);
HeapVector<Member<Element>> elements;
collectCandidates(CustomElementDescriptor("a-a", "a-a"), &elements);
EXPECT_TRUE(elements.isEmpty())
<< "no candidates should have been found, but we have "
<< elements.size();
EXPECT_FALSE(elements.contains(element))
<< "the out-of-document candidate should not have been found";
}
TEST_F(CustomElementRegistryTest,
collectCandidates_shouldNotIncludeElementsInDifferentDocument) {
Element* element = CreateElement("a-a").inDocument(&document());
registry().addCandidate(element);
Document* otherDocument = HTMLDocument::create();
otherDocument->appendChild(element);
EXPECT_EQ(otherDocument, element->ownerDocument())
<< "sanity: another document should have adopted an element on append";
HeapVector<Member<Element>> elements;
collectCandidates(CustomElementDescriptor("a-a", "a-a"), &elements);
EXPECT_TRUE(elements.isEmpty())
<< "no candidates should have been found, but we have "
<< elements.size();
EXPECT_FALSE(elements.contains(element))
<< "the adopted-away candidate should not have been found";
}
TEST_F(CustomElementRegistryTest,
collectCandidates_shouldOnlyIncludeCandidatesMatchingDescriptor) {
CustomElementDescriptor descriptor("hello-world", "hello-world");
// Does not match: namespace is not HTML
Element* elementA = CreateElement("hello-world")
.inDocument(&document())
.inNamespace("data:text/date,1981-03-10");
// Matches
Element* elementB = CreateElement("hello-world").inDocument(&document());
// Does not match: local name is not hello-world
Element* elementC = CreateElement("button")
.inDocument(&document())
.withIsAttribute("hello-world");
document().documentElement()->appendChild(elementA);
elementA->appendChild(elementB);
elementA->appendChild(elementC);
registry().addCandidate(elementA);
registry().addCandidate(elementB);
registry().addCandidate(elementC);
HeapVector<Member<Element>> elements;
collectCandidates(descriptor, &elements);
EXPECT_EQ(1u, elements.size())
<< "only one candidates should have been found";
EXPECT_EQ(elementB, elements[0])
<< "the matching element should have been found";
}
TEST_F(CustomElementRegistryTest, collectCandidates_oneCandidate) {
Element* element = CreateElement("a-a").inDocument(&document());
registry().addCandidate(element);
document().documentElement()->appendChild(element);
HeapVector<Member<Element>> elements;
collectCandidates(CustomElementDescriptor("a-a", "a-a"), &elements);
EXPECT_EQ(1u, elements.size())
<< "exactly one candidate should have been found";
EXPECT_TRUE(elements.contains(element))
<< "the candidate should be the element that was added";
}
TEST_F(CustomElementRegistryTest, collectCandidates_shouldBeInDocumentOrder) {
CreateElement factory = CreateElement("a-a");
factory.inDocument(&document());
Element* elementA = factory.withId("a");
Element* elementB = factory.withId("b");
Element* elementC = factory.withId("c");
registry().addCandidate(elementB);
registry().addCandidate(elementA);
registry().addCandidate(elementC);
document().documentElement()->appendChild(elementA);
elementA->appendChild(elementB);
document().documentElement()->appendChild(elementC);
HeapVector<Member<Element>> elements;
collectCandidates(CustomElementDescriptor("a-a", "a-a"), &elements);
EXPECT_EQ(elementA, elements[0].get());
EXPECT_EQ(elementB, elements[1].get());
EXPECT_EQ(elementC, elements[2].get());
}
// Classes which use trace macros cannot be local because of the
// traceImpl template.
class LogUpgradeDefinition : public TestCustomElementDefinition {
WTF_MAKE_NONCOPYABLE(LogUpgradeDefinition);
public:
LogUpgradeDefinition(const CustomElementDescriptor& descriptor)
: TestCustomElementDefinition(
descriptor,
{
"attr1", "attr2", HTMLNames::contenteditableAttr.localName(),
}) {}
DEFINE_INLINE_VIRTUAL_TRACE() {
TestCustomElementDefinition::trace(visitor);
visitor->trace(m_element);
visitor->trace(m_adopted);
}
// TODO(dominicc): Make this class collect a vector of what's
// upgraded; it will be useful in more tests.
Member<Element> m_element;
enum MethodType {
Constructor,
ConnectedCallback,
DisconnectedCallback,
AdoptedCallback,
AttributeChangedCallback,
};
Vector<MethodType> m_logs;
struct AttributeChanged {
QualifiedName name;
AtomicString oldValue;
AtomicString newValue;
};
Vector<AttributeChanged> m_attributeChanged;
struct Adopted : public GarbageCollected<Adopted> {
Adopted(Document* oldOwner, Document* newOwner)
: m_oldOwner(oldOwner), m_newOwner(newOwner) {}
Member<Document> m_oldOwner;
Member<Document> m_newOwner;
DEFINE_INLINE_TRACE() {
visitor->trace(m_oldOwner);
visitor->trace(m_newOwner);
}
};
HeapVector<Member<Adopted>> m_adopted;
void clear() {
m_logs.clear();
m_attributeChanged.clear();
}
bool runConstructor(Element* element) override {
m_logs.append(Constructor);
m_element = element;
return TestCustomElementDefinition::runConstructor(element);
}
bool hasConnectedCallback() const override { return true; }
bool hasDisconnectedCallback() const override { return true; }
bool hasAdoptedCallback() const override { return true; }
void runConnectedCallback(Element* element) override {
m_logs.append(ConnectedCallback);
EXPECT_EQ(element, m_element);
}
void runDisconnectedCallback(Element* element) override {
m_logs.append(DisconnectedCallback);
EXPECT_EQ(element, m_element);
}
void runAdoptedCallback(Element* element,
Document* oldOwner,
Document* newOwner) override {
m_logs.append(AdoptedCallback);
EXPECT_EQ(element, m_element);
m_adopted.append(new Adopted(oldOwner, newOwner));
}
void runAttributeChangedCallback(Element* element,
const QualifiedName& name,
const AtomicString& oldValue,
const AtomicString& newValue) override {
m_logs.append(AttributeChangedCallback);
EXPECT_EQ(element, m_element);
m_attributeChanged.append(AttributeChanged{name, oldValue, newValue});
}
};
class LogUpgradeBuilder final : public TestCustomElementDefinitionBuilder {
STACK_ALLOCATED();
WTF_MAKE_NONCOPYABLE(LogUpgradeBuilder);
public:
LogUpgradeBuilder() {}
CustomElementDefinition* build(
const CustomElementDescriptor& descriptor) override {
return new LogUpgradeDefinition(descriptor);
}
};
TEST_F(CustomElementRegistryTest, define_upgradesInDocumentElements) {
ScriptForbiddenScope doNotRelyOnScript;
Element* element = CreateElement("a-a").inDocument(&document());
element->setAttribute(
QualifiedName(nullAtom, "attr1", HTMLNames::xhtmlNamespaceURI), "v1");
element->setBooleanAttribute(HTMLNames::contenteditableAttr, true);
document().documentElement()->appendChild(element);
LogUpgradeBuilder builder;
NonThrowableExceptionState shouldNotThrow;
{
CEReactionsScope reactions;
registry().define("a-a", builder, ElementDefinitionOptions(),
shouldNotThrow);
}
LogUpgradeDefinition* definition =
static_cast<LogUpgradeDefinition*>(registry().definitionForName("a-a"));
EXPECT_EQ(LogUpgradeDefinition::Constructor, definition->m_logs[0])
<< "defining the element should have 'upgraded' the existing element";
EXPECT_EQ(element, definition->m_element)
<< "the existing a-a element should have been upgraded";
EXPECT_EQ(LogUpgradeDefinition::AttributeChangedCallback,
definition->m_logs[1])
<< "Upgrade should invoke attributeChangedCallback for all attributes";
EXPECT_EQ("attr1", definition->m_attributeChanged[0].name);
EXPECT_EQ(nullAtom, definition->m_attributeChanged[0].oldValue);
EXPECT_EQ("v1", definition->m_attributeChanged[0].newValue);
EXPECT_EQ(LogUpgradeDefinition::AttributeChangedCallback,
definition->m_logs[2])
<< "Upgrade should invoke attributeChangedCallback for all attributes";
EXPECT_EQ("contenteditable", definition->m_attributeChanged[1].name);
EXPECT_EQ(nullAtom, definition->m_attributeChanged[1].oldValue);
EXPECT_EQ(emptyAtom, definition->m_attributeChanged[1].newValue);
EXPECT_EQ(2u, definition->m_attributeChanged.size())
<< "Upgrade should invoke attributeChangedCallback for all attributes";
EXPECT_EQ(LogUpgradeDefinition::ConnectedCallback, definition->m_logs[3])
<< "upgrade should invoke connectedCallback";
EXPECT_EQ(4u, definition->m_logs.size())
<< "upgrade should not invoke other callbacks";
}
TEST_F(CustomElementRegistryTest, attributeChangedCallback) {
ScriptForbiddenScope doNotRelyOnScript;
Element* element = CreateElement("a-a").inDocument(&document());
document().documentElement()->appendChild(element);
LogUpgradeBuilder builder;
NonThrowableExceptionState shouldNotThrow;
{
CEReactionsScope reactions;
registry().define("a-a", builder, ElementDefinitionOptions(),
shouldNotThrow);
}
LogUpgradeDefinition* definition =
static_cast<LogUpgradeDefinition*>(registry().definitionForName("a-a"));
definition->clear();
{
CEReactionsScope reactions;
element->setAttribute(
QualifiedName(nullAtom, "attr2", HTMLNames::xhtmlNamespaceURI), "v2");
}
EXPECT_EQ(LogUpgradeDefinition::AttributeChangedCallback,
definition->m_logs[0])
<< "Adding an attribute should invoke attributeChangedCallback";
EXPECT_EQ(1u, definition->m_attributeChanged.size())
<< "Adding an attribute should invoke attributeChangedCallback";
EXPECT_EQ("attr2", definition->m_attributeChanged[0].name);
EXPECT_EQ(nullAtom, definition->m_attributeChanged[0].oldValue);
EXPECT_EQ("v2", definition->m_attributeChanged[0].newValue);
EXPECT_EQ(1u, definition->m_logs.size())
<< "upgrade should not invoke other callbacks";
}
TEST_F(CustomElementRegistryTest, disconnectedCallback) {
ScriptForbiddenScope doNotRelyOnScript;
Element* element = CreateElement("a-a").inDocument(&document());
document().documentElement()->appendChild(element);
LogUpgradeBuilder builder;
NonThrowableExceptionState shouldNotThrow;
{
CEReactionsScope reactions;
registry().define("a-a", builder, ElementDefinitionOptions(),
shouldNotThrow);
}
LogUpgradeDefinition* definition =
static_cast<LogUpgradeDefinition*>(registry().definitionForName("a-a"));
definition->clear();
{
CEReactionsScope reactions;
element->remove(shouldNotThrow);
}
EXPECT_EQ(LogUpgradeDefinition::DisconnectedCallback, definition->m_logs[0])
<< "remove() should invoke disconnectedCallback";
EXPECT_EQ(1u, definition->m_logs.size())
<< "remove() should not invoke other callbacks";
}
TEST_F(CustomElementRegistryTest, adoptedCallback) {
ScriptForbiddenScope doNotRelyOnScript;
Element* element = CreateElement("a-a").inDocument(&document());
document().documentElement()->appendChild(element);
LogUpgradeBuilder builder;
NonThrowableExceptionState shouldNotThrow;
{
CEReactionsScope reactions;
registry().define("a-a", builder, ElementDefinitionOptions(),
shouldNotThrow);
}
LogUpgradeDefinition* definition =
static_cast<LogUpgradeDefinition*>(registry().definitionForName("a-a"));
definition->clear();
Document* otherDocument = HTMLDocument::create();
{
CEReactionsScope reactions;
otherDocument->adoptNode(element, ASSERT_NO_EXCEPTION);
}
EXPECT_EQ(LogUpgradeDefinition::DisconnectedCallback, definition->m_logs[0])
<< "adoptNode() should invoke disconnectedCallback";
EXPECT_EQ(LogUpgradeDefinition::AdoptedCallback, definition->m_logs[1])
<< "adoptNode() should invoke adoptedCallback";
EXPECT_EQ(&document(), definition->m_adopted[0]->m_oldOwner.get())
<< "adoptedCallback should have been passed the old owner document";
EXPECT_EQ(otherDocument, definition->m_adopted[0]->m_newOwner.get())
<< "adoptedCallback should have been passed the new owner document";
EXPECT_EQ(2u, definition->m_logs.size())
<< "adoptNode() should not invoke other callbacks";
}
TEST_F(CustomElementRegistryTest, lookupCustomElementDefinition) {
NonThrowableExceptionState shouldNotThrow;
TestCustomElementDefinitionBuilder builder;
CustomElementDefinition* definitionA = registry().define(
"a-a", builder, ElementDefinitionOptions(), shouldNotThrow);
ElementDefinitionOptions options;
options.setExtends("div");
CustomElementDefinition* definitionB =
registry().define("b-b", builder, options, shouldNotThrow);
// look up defined autonomous custom element
CustomElementDefinition* definition = registry().definitionFor(
CustomElementDescriptor(CustomElementDescriptor("a-a", "a-a")));
EXPECT_NE(nullptr, definition) << "a-a, a-a should be registered";
EXPECT_EQ(definitionA, definition);
// look up undefined autonomous custom element
definition = registry().definitionFor(CustomElementDescriptor("a-a", "div"));
EXPECT_EQ(nullptr, definition) << "a-a, div should not be registered";
// look up defined customized built-in element
definition = registry().definitionFor(CustomElementDescriptor("b-b", "div"));
EXPECT_NE(nullptr, definition) << "b-b, div should be registered";
EXPECT_EQ(definitionB, definition);
// look up undefined customized built-in element
definition = registry().definitionFor(CustomElementDescriptor("a-a", "div"));
EXPECT_EQ(nullptr, definition) << "a-a, div should not be registered";
}
// TODO(dominicc): Add tests which adjust the "is" attribute when type
// extensions are implemented.
} // namespace blink