Commit d17a8937 authored by kojii's avatar kojii Committed by Commit bot

Add callbacks to ScriptCustomElementDefinition

This patch adds callbacks and observedAttributes to
ScriptCustomElementDefinition, as defined in the Element Definition
spec[1].

[1] https://html.spec.whatwg.org/multipage/scripting.html#element-definition

BUG=594918
TEST=imported/wpt/custom-elements/custom-elements-registry/define.html

Review-Url: https://codereview.chromium.org/2024073002
Cr-Commit-Position: refs/heads/master@{#398243}
parent d2861a3e
...@@ -73,53 +73,27 @@ FAIL If extends is spacer, should throw a NotSupportedError assert_throws: funct ...@@ -73,53 +73,27 @@ FAIL If extends is spacer, should throw a NotSupportedError assert_throws: funct
FAIL If extends is elementnametobeunknownelement, should throw a NotSupportedError assert_throws: function "() => { FAIL If extends is elementnametobeunknownelement, should throw a NotSupportedError assert_throws: function "() => {
customElements.define('test-define-extend-' + name, class {}, { extends: name }); customElements.define('test-define-extend-' + name, class {}, { extends: name });
}" did not throw }" did not throw
FAIL If constructor.observedAttributes throws, should rethrow assert_throws: function "() => { PASS If constructor.observedAttributes throws, should rethrow
customElements.define('test-define-observedattributes-rethrow', C);
}" did not throw
PASS If constructor.prototype throws, should rethrow PASS If constructor.prototype throws, should rethrow
PASS If Type(constructor.prototype) is undefined, should throw a TypeError PASS If Type(constructor.prototype) is undefined, should throw a TypeError
PASS If Type(constructor.prototype) is string, should throw a TypeError PASS If Type(constructor.prototype) is string, should throw a TypeError
FAIL If constructor.prototype.connectedCallback throws, should rethrow assert_throws: function "() => { PASS If constructor.prototype.connectedCallback throws, should rethrow
customElements.define(`test-define-${name.toLowerCase()}-rethrow`, C);
}" did not throw
PASS If constructor.prototype.connectedCallback is undefined, should succeed PASS If constructor.prototype.connectedCallback is undefined, should succeed
PASS If constructor.prototype.connectedCallback is function, should succeed PASS If constructor.prototype.connectedCallback is function, should succeed
FAIL If constructor.prototype.connectedCallback is null, should throw a TypeError assert_throws: function "() => { PASS If constructor.prototype.connectedCallback is null, should throw a TypeError
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C); PASS If constructor.prototype.connectedCallback is object, should throw a TypeError
}" did not throw PASS If constructor.prototype.connectedCallback is integer, should throw a TypeError
FAIL If constructor.prototype.connectedCallback is object, should throw a TypeError assert_throws: function "() => { PASS If constructor.prototype.disconnectedCallback throws, should rethrow
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
FAIL If constructor.prototype.connectedCallback is integer, should throw a TypeError assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
FAIL If constructor.prototype.disconnectedCallback throws, should rethrow assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-rethrow`, C);
}" did not throw
PASS If constructor.prototype.disconnectedCallback is undefined, should succeed PASS If constructor.prototype.disconnectedCallback is undefined, should succeed
PASS If constructor.prototype.disconnectedCallback is function, should succeed PASS If constructor.prototype.disconnectedCallback is function, should succeed
FAIL If constructor.prototype.disconnectedCallback is null, should throw a TypeError assert_throws: function "() => { PASS If constructor.prototype.disconnectedCallback is null, should throw a TypeError
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C); PASS If constructor.prototype.disconnectedCallback is object, should throw a TypeError
}" did not throw PASS If constructor.prototype.disconnectedCallback is integer, should throw a TypeError
FAIL If constructor.prototype.disconnectedCallback is object, should throw a TypeError assert_throws: function "() => { PASS If constructor.prototype.attributeChangedCallback throws, should rethrow
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
FAIL If constructor.prototype.disconnectedCallback is integer, should throw a TypeError assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
FAIL If constructor.prototype.attributeChangedCallback throws, should rethrow assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-rethrow`, C);
}" did not throw
PASS If constructor.prototype.attributeChangedCallback is undefined, should succeed PASS If constructor.prototype.attributeChangedCallback is undefined, should succeed
PASS If constructor.prototype.attributeChangedCallback is function, should succeed PASS If constructor.prototype.attributeChangedCallback is function, should succeed
FAIL If constructor.prototype.attributeChangedCallback is null, should throw a TypeError assert_throws: function "() => { PASS If constructor.prototype.attributeChangedCallback is null, should throw a TypeError
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C); PASS If constructor.prototype.attributeChangedCallback is object, should throw a TypeError
}" did not throw PASS If constructor.prototype.attributeChangedCallback is integer, should throw a TypeError
FAIL If constructor.prototype.attributeChangedCallback is object, should throw a TypeError assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
FAIL If constructor.prototype.attributeChangedCallback is integer, should throw a TypeError assert_throws: function "() => {
customElements.define(`test-define-${name.toLowerCase()}-${data.name}`, C);
}" did not throw
Harness: the test ran to completion. Harness: the test ran to completion.
...@@ -80,19 +80,41 @@ ScriptCustomElementDefinition* ScriptCustomElementDefinition::forConstructor( ...@@ -80,19 +80,41 @@ ScriptCustomElementDefinition* ScriptCustomElementDefinition::forConstructor(
return static_cast<ScriptCustomElementDefinition*>(definition); return static_cast<ScriptCustomElementDefinition*>(definition);
} }
static void keepAlive(v8::Local<v8::Array>& array, uint32_t index,
const v8::Local<v8::Object>& value,
ScopedPersistent<v8::Object>& persistent,
ScriptState* scriptState)
{
if (value.IsEmpty())
return;
v8CallOrCrash(array->Set(scriptState->context(), index, value));
persistent.set(scriptState->isolate(), value);
persistent.setPhantom();
}
ScriptCustomElementDefinition* ScriptCustomElementDefinition::create( ScriptCustomElementDefinition* ScriptCustomElementDefinition::create(
ScriptState* scriptState, ScriptState* scriptState,
CustomElementsRegistry* registry, CustomElementsRegistry* registry,
const CustomElementDescriptor& descriptor, const CustomElementDescriptor& descriptor,
const v8::Local<v8::Object>& constructor, const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype) const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Object>& connectedCallback,
const v8::Local<v8::Object>& disconnectedCallback,
const v8::Local<v8::Object>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes)
{ {
ScriptCustomElementDefinition* definition = ScriptCustomElementDefinition* definition =
new ScriptCustomElementDefinition( new ScriptCustomElementDefinition(
scriptState, scriptState,
descriptor, descriptor,
constructor, constructor,
prototype); prototype,
connectedCallback,
disconnectedCallback,
attributeChangedCallback,
observedAttributes);
// Add a constructor -> name mapping to the registry. // Add a constructor -> name mapping to the registry.
v8::Local<v8::Value> nameValue = v8::Local<v8::Value> nameValue =
...@@ -100,11 +122,16 @@ ScriptCustomElementDefinition* ScriptCustomElementDefinition::create( ...@@ -100,11 +122,16 @@ ScriptCustomElementDefinition* ScriptCustomElementDefinition::create(
v8::Local<v8::Map> map = v8::Local<v8::Map> map =
ensureCustomElementsRegistryMap(scriptState, registry); ensureCustomElementsRegistryMap(scriptState, registry);
v8CallOrCrash(map->Set(scriptState->context(), constructor, nameValue)); v8CallOrCrash(map->Set(scriptState->context(), constructor, nameValue));
// We add the prototype here to keep it alive; we make it a value definition->m_constructor.setPhantom();
// not a key so authors cannot return another constructor as a
// prototype to overwrite a constructor in this map. We use the // We add the prototype and callbacks here to keep them alive. We use the
// name because it is unique per-registry. // name as the key because it is unique per-registry.
v8CallOrCrash(map->Set(scriptState->context(), nameValue, prototype)); v8::Local<v8::Array> array = v8::Array::New(scriptState->isolate(), 4);
keepAlive(array, 0, prototype, definition->m_prototype, scriptState);
keepAlive(array, 1, connectedCallback, definition->m_connectedCallback, scriptState);
keepAlive(array, 2, disconnectedCallback, definition->m_disconnectedCallback, scriptState);
keepAlive(array, 3, attributeChangedCallback, definition->m_attributeChangedCallback, scriptState);
v8CallOrCrash(map->Set(scriptState->context(), nameValue, array));
return definition; return definition;
} }
...@@ -113,17 +140,16 @@ ScriptCustomElementDefinition::ScriptCustomElementDefinition( ...@@ -113,17 +140,16 @@ ScriptCustomElementDefinition::ScriptCustomElementDefinition(
ScriptState* scriptState, ScriptState* scriptState,
const CustomElementDescriptor& descriptor, const CustomElementDescriptor& descriptor,
const v8::Local<v8::Object>& constructor, const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype) const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Object>& connectedCallback,
const v8::Local<v8::Object>& disconnectedCallback,
const v8::Local<v8::Object>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes)
: CustomElementDefinition(descriptor) : CustomElementDefinition(descriptor)
, m_scriptState(scriptState) , m_scriptState(scriptState)
, m_constructor(scriptState->isolate(), constructor) , m_constructor(scriptState->isolate(), constructor)
, m_prototype(scriptState->isolate(), prototype) , m_observedAttributes(observedAttributes)
{ {
// These objects are kept alive by references from the
// CustomElementsRegistry wrapper set up by
// ScriptCustomElementDefinition::create.
m_constructor.setPhantom();
m_prototype.setPhantom();
} }
// https://html.spec.whatwg.org/multipage/scripting.html#upgrades // https://html.spec.whatwg.org/multipage/scripting.html#upgrades
......
...@@ -10,8 +10,11 @@ ...@@ -10,8 +10,11 @@
#include "core/CoreExport.h" #include "core/CoreExport.h"
#include "core/dom/custom/CustomElementDefinition.h" #include "core/dom/custom/CustomElementDefinition.h"
#include "v8.h" #include "v8.h"
#include "wtf/HashSet.h"
#include "wtf/Noncopyable.h" #include "wtf/Noncopyable.h"
#include "wtf/RefPtr.h" #include "wtf/RefPtr.h"
#include "wtf/text/AtomicString.h"
#include "wtf/text/AtomicStringHash.h"
namespace blink { namespace blink {
...@@ -32,7 +35,11 @@ public: ...@@ -32,7 +35,11 @@ public:
CustomElementsRegistry*, CustomElementsRegistry*,
const CustomElementDescriptor&, const CustomElementDescriptor&,
const v8::Local<v8::Object>& constructor, const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype); const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Object>& connectedCallback,
const v8::Local<v8::Object>& disconnectedCallback,
const v8::Local<v8::Object>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes);
virtual ~ScriptCustomElementDefinition() = default; virtual ~ScriptCustomElementDefinition() = default;
...@@ -44,7 +51,11 @@ private: ...@@ -44,7 +51,11 @@ private:
ScriptState*, ScriptState*,
const CustomElementDescriptor&, const CustomElementDescriptor&,
const v8::Local<v8::Object>& constructor, const v8::Local<v8::Object>& constructor,
const v8::Local<v8::Object>& prototype); const v8::Local<v8::Object>& prototype,
const v8::Local<v8::Object>& connectedCallback,
const v8::Local<v8::Object>& disconnectedCallback,
const v8::Local<v8::Object>& attributeChangedCallback,
const HashSet<AtomicString>& observedAttributes);
// Implementations of |CustomElementDefinition| // Implementations of |CustomElementDefinition|
ScriptValue getConstructorForScript() final; ScriptValue getConstructorForScript() final;
...@@ -53,6 +64,10 @@ private: ...@@ -53,6 +64,10 @@ private:
RefPtr<ScriptState> m_scriptState; RefPtr<ScriptState> m_scriptState;
ScopedPersistent<v8::Object> m_constructor; ScopedPersistent<v8::Object> m_constructor;
ScopedPersistent<v8::Object> m_prototype; ScopedPersistent<v8::Object> m_prototype;
ScopedPersistent<v8::Object> m_connectedCallback;
ScopedPersistent<v8::Object> m_disconnectedCallback;
ScopedPersistent<v8::Object> m_attributeChangedCallback;
HashSet<AtomicString> m_observedAttributes;
}; };
} // namespace blink } // namespace blink
......
...@@ -81,17 +81,26 @@ bool ScriptCustomElementDefinitionBuilder::checkConstructorNotRegistered() ...@@ -81,17 +81,26 @@ bool ScriptCustomElementDefinitionBuilder::checkConstructorNotRegistered()
return true; return true;
} }
bool ScriptCustomElementDefinitionBuilder::checkPrototype() bool ScriptCustomElementDefinitionBuilder::valueForName(
const v8::Local<v8::Object>& object, const String& name,
v8::Local<v8::Value>& value) const
{ {
v8::Isolate* isolate = m_scriptState->isolate(); v8::Isolate* isolate = m_scriptState->isolate();
v8::Local<v8::Context> context = m_scriptState->context(); v8::Local<v8::Context> context = m_scriptState->context();
v8::Local<v8::String> prototypeString = v8::Local<v8::String> nameString = v8String(isolate, name);
v8AtomicString(isolate, "prototype"); v8::TryCatch tryCatch(isolate);
v8::Local<v8::Value> prototypeValue; if (!v8Call(object->Get(context, nameString), value, tryCatch)) {
if (!v8Call( m_exceptionState.rethrowV8Exception(tryCatch.Exception());
m_constructor->Get(context, prototypeString), prototypeValue)) {
return false; return false;
} }
return true;
}
bool ScriptCustomElementDefinitionBuilder::checkPrototype()
{
v8::Local<v8::Value> prototypeValue;
if (!valueForName(m_constructor, "prototype", prototypeValue))
return false;
if (!prototypeValue->IsObject()) { if (!prototypeValue->IsObject()) {
m_exceptionState.throwTypeError( m_exceptionState.throwTypeError(
"constructor prototype is not an object"); "constructor prototype is not an object");
...@@ -103,6 +112,62 @@ bool ScriptCustomElementDefinitionBuilder::checkPrototype() ...@@ -103,6 +112,62 @@ bool ScriptCustomElementDefinitionBuilder::checkPrototype()
return true; return true;
} }
bool ScriptCustomElementDefinitionBuilder::callableForName(const String& name,
v8::Local<v8::Object>& callback) const
{
v8::Local<v8::Value> value;
if (!valueForName(m_prototype, name, value))
return false;
// "undefined" means "omitted", so return true.
if (value->IsUndefined())
return true;
if (!value->IsObject()) {
m_exceptionState.throwTypeError(
String::format("\"%s\" is not an object", name.ascii().data()));
return false;
}
callback = value.As<v8::Object>();
if (!callback->IsCallable()) {
m_exceptionState.throwTypeError(
String::format("\"%s\" is not callable", name.ascii().data()));
return false;
}
return true;
}
bool ScriptCustomElementDefinitionBuilder::retrieveObservedAttributes()
{
const String kObservedAttributes = "observedAttributes";
v8::Local<v8::Value> observedAttributesValue;
if (!valueForName(m_constructor, kObservedAttributes, observedAttributesValue))
return false;
if (observedAttributesValue->IsUndefined())
return true;
Vector<AtomicString> list = toImplArray<Vector<AtomicString>>(
observedAttributesValue, 0, m_scriptState->isolate(), m_exceptionState);
if (m_exceptionState.hadException())
return false;
if (list.isEmpty())
return true;
m_observedAttributes.reserveCapacityForSize(list.size());
for (const auto& attribute : list)
m_observedAttributes.add(attribute);
return true;
}
bool ScriptCustomElementDefinitionBuilder::rememberOriginalProperties()
{
// Spec requires to use values of these properties at the point
// CustomElementDefinition is built, even if JS changes them afterwards.
const String kConnectedCallback = "connectedCallback";
const String kDisconnectedCallback = "disconnectedCallback";
const String kAttributeChangedCallback = "attributeChangedCallback";
return retrieveObservedAttributes()
&& callableForName(kConnectedCallback, m_connectedCallback)
&& callableForName(kDisconnectedCallback, m_disconnectedCallback)
&& callableForName(kAttributeChangedCallback, m_attributeChangedCallback);
}
CustomElementDefinition* ScriptCustomElementDefinitionBuilder::build( CustomElementDefinition* ScriptCustomElementDefinitionBuilder::build(
const CustomElementDescriptor& descriptor) const CustomElementDescriptor& descriptor)
{ {
...@@ -111,7 +176,11 @@ CustomElementDefinition* ScriptCustomElementDefinitionBuilder::build( ...@@ -111,7 +176,11 @@ CustomElementDefinition* ScriptCustomElementDefinitionBuilder::build(
m_registry, m_registry,
descriptor, descriptor,
m_constructor, m_constructor,
m_prototype); m_prototype,
m_connectedCallback,
m_disconnectedCallback,
m_attributeChangedCallback,
m_observedAttributes);
} }
} // namespace blink } // namespace blink
...@@ -10,8 +10,11 @@ ...@@ -10,8 +10,11 @@
#include "platform/heap/Handle.h" #include "platform/heap/Handle.h"
#include "v8.h" #include "v8.h"
#include "wtf/Allocator.h" #include "wtf/Allocator.h"
#include "wtf/HashSet.h"
#include "wtf/Noncopyable.h" #include "wtf/Noncopyable.h"
#include "wtf/RefPtr.h" #include "wtf/RefPtr.h"
#include "wtf/text/AtomicString.h"
#include "wtf/text/AtomicStringHash.h"
namespace blink { namespace blink {
...@@ -35,6 +38,7 @@ public: ...@@ -35,6 +38,7 @@ public:
bool checkConstructorIntrinsics() override; bool checkConstructorIntrinsics() override;
bool checkConstructorNotRegistered() override; bool checkConstructorNotRegistered() override;
bool checkPrototype() override; bool checkPrototype() override;
bool rememberOriginalProperties() override;
CustomElementDefinition* build(const CustomElementDescriptor&) override; CustomElementDefinition* build(const CustomElementDescriptor&) override;
private: private:
...@@ -46,7 +50,15 @@ private: ...@@ -46,7 +50,15 @@ private:
v8::Local<v8::Value> m_constructorValue; v8::Local<v8::Value> m_constructorValue;
v8::Local<v8::Object> m_constructor; v8::Local<v8::Object> m_constructor;
v8::Local<v8::Object> m_prototype; v8::Local<v8::Object> m_prototype;
v8::Local<v8::Object> m_connectedCallback;
v8::Local<v8::Object> m_disconnectedCallback;
v8::Local<v8::Object> m_attributeChangedCallback;
HashSet<AtomicString> m_observedAttributes;
ExceptionState& m_exceptionState; ExceptionState& m_exceptionState;
bool valueForName(const v8::Local<v8::Object>&, const String&, v8::Local<v8::Value>&) const;
bool callableForName(const String&, v8::Local<v8::Object>&) const;
bool retrieveObservedAttributes();
}; };
} // namespace blink } // namespace blink
......
...@@ -774,6 +774,17 @@ struct NativeValueTraits<String> { ...@@ -774,6 +774,17 @@ struct NativeValueTraits<String> {
} }
}; };
template<>
struct NativeValueTraits<AtomicString> {
static inline AtomicString nativeValue(v8::Isolate* isolate, v8::Local<v8::Value> value, ExceptionState& exceptionState)
{
V8StringResource<> stringValue(value);
if (!stringValue.prepare(exceptionState))
return AtomicString();
return stringValue;
}
};
template<> template<>
struct NativeValueTraits<int> { struct NativeValueTraits<int> {
static inline int nativeValue(v8::Isolate* isolate, v8::Local<v8::Value> value, ExceptionState& exceptionState) static inline int nativeValue(v8::Isolate* isolate, v8::Local<v8::Value> value, ExceptionState& exceptionState)
......
...@@ -43,6 +43,10 @@ public: ...@@ -43,6 +43,10 @@ public:
// processing should not proceed. // processing should not proceed.
virtual bool checkPrototype() = 0; virtual bool checkPrototype() = 0;
// Cache properties for build to use. Return false if processing
// should not proceed.
virtual bool rememberOriginalProperties() = 0;
// Produce the definition. This must produce a definition. // Produce the definition. This must produce a definition.
virtual CustomElementDefinition* build(const CustomElementDescriptor&) = 0; virtual CustomElementDefinition* build(const CustomElementDescriptor&) = 0;
}; };
......
...@@ -123,7 +123,10 @@ void CustomElementsRegistry::define( ...@@ -123,7 +123,10 @@ void CustomElementsRegistry::define(
// TODO(dominicc): Implement steps: // TODO(dominicc): Implement steps:
// 5: localName // 5: localName
// 6-7: extends processing // 6-7: extends processing
// 8-9: observed attributes caching
// 8-9: observed attributes caching is done below, together with callbacks.
// TODO(kojii): https://github.com/whatwg/html/issues/1373 for the ordering.
// When it's resolved, revisit if this code needs changes.
// TODO(dominicc): Add a test where the prototype getter destroys // TODO(dominicc): Add a test where the prototype getter destroys
// the context. // the context.
...@@ -131,11 +134,14 @@ void CustomElementsRegistry::define( ...@@ -131,11 +134,14 @@ void CustomElementsRegistry::define(
if (!builder.checkPrototype()) if (!builder.checkPrototype())
return; return;
// TODO(dominicc): Implement steps: // 8-9: observed attributes caching
// 12-13: connected callback // 12-13: connected callback
// 14-15: disconnected callback // 14-15: disconnected callback
// 16-17: attribute changed callback // 16-17: attribute changed callback
if (!builder.rememberOriginalProperties())
return;
// TODO(dominicc): Add a test where retrieving the prototype // TODO(dominicc): Add a test where retrieving the prototype
// recursively calls define with the same name. // recursively calls define with the same name.
......
...@@ -286,6 +286,7 @@ public: ...@@ -286,6 +286,7 @@ public:
bool checkConstructorIntrinsics() override { return true; } bool checkConstructorIntrinsics() override { return true; }
bool checkConstructorNotRegistered() override { return true; } bool checkConstructorNotRegistered() override { return true; }
bool checkPrototype() override { return true; } bool checkPrototype() override { return true; }
bool rememberOriginalProperties() override { return true; }
CustomElementDefinition* build( CustomElementDefinition* build(
const CustomElementDescriptor& descriptor) { const CustomElementDescriptor& descriptor) {
return new LogUpgradeDefinition(descriptor); return new LogUpgradeDefinition(descriptor);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment