mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-01 13:49:16 +00:00
LibWeb: Fire slotchange events when a slot is changed
This commit is contained in:
parent
84ecaaa75c
commit
b543523717
Notes:
github-actions[bot]
2025-03-10 18:38:25 +00:00
Author: https://github.com/shannonbooth
Commit: b543523717
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3871
Reviewed-by: https://github.com/trflynn89 ✅
7 changed files with 717 additions and 5 deletions
|
@ -28,6 +28,7 @@
|
|||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/HTML/CustomElements/CustomElementDefinition.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
#include <LibWeb/HTML/HTMLSlotElement.h>
|
||||
#include <LibWeb/HTML/Location.h>
|
||||
#include <LibWeb/HTML/PromiseRejectionEvent.h>
|
||||
#include <LibWeb/HTML/Scripting/Agent.h>
|
||||
|
@ -677,9 +678,9 @@ void queue_mutation_observer_microtask(DOM::Document const& document)
|
|||
for (auto& observer : surrounding_agent.mutation_observers)
|
||||
notify_set.append(&observer);
|
||||
|
||||
// FIXME: 3. Let signalSet be a clone of the surrounding agent’s signal slots.
|
||||
|
||||
// FIXME: 4. Empty the surrounding agent’s signal slots.
|
||||
// 3. Let signalSet be a clone of the surrounding agent’s signal slots.
|
||||
// 4. Empty the surrounding agent’s signal slots.
|
||||
auto signal_set = move(surrounding_agent.signal_slots);
|
||||
|
||||
// 5. For each mo of notifySet:
|
||||
for (auto& mutation_observer : notify_set) {
|
||||
|
@ -716,7 +717,12 @@ void queue_mutation_observer_microtask(DOM::Document const& document)
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: 6. For each slot of signalSet, fire an event named slotchange, with its bubbles attribute set to true, at slot.
|
||||
// 6. For each slot of signalSet, fire an event named slotchange, with its bubbles attribute set to true, at slot.
|
||||
for (auto& slot : signal_set) {
|
||||
DOM::EventInit event_init;
|
||||
event_init.bubbles = true;
|
||||
slot->dispatch_event(DOM::Event::create(slot->realm(), HTML::EventNames::slotchange, event_init));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -206,7 +206,8 @@ void assign_a_slot(Slottable const& slottable)
|
|||
// https://dom.spec.whatwg.org/#signal-a-slot-change
|
||||
void signal_a_slot_change(GC::Ref<HTML::HTMLSlotElement> slottable)
|
||||
{
|
||||
// FIXME: 1. Append slot to slot’s relevant agent’s signal slots.
|
||||
// 1. Append slot to slot’s relevant agent’s signal slots.
|
||||
HTML::relevant_agent(slottable).signal_slots.append(slottable);
|
||||
|
||||
// 2. Queue a mutation observer microtask.
|
||||
Bindings::queue_mutation_observer_microtask(slottable->document());
|
||||
|
|
|
@ -32,6 +32,10 @@ struct Agent {
|
|||
// Each similar-origin window agent has a custom element reactions stack, which is initially empty.
|
||||
CustomElementReactionsStack custom_element_reactions_stack {};
|
||||
|
||||
// https://dom.spec.whatwg.org/#signal-slot-list
|
||||
// Each similar-origin window agent has signal slots (a set of slots), which is initially empty. [HTML]
|
||||
Vector<GC::Root<HTML::HTMLSlotElement>> signal_slots;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/custom-elements.html#current-element-queue
|
||||
// A similar-origin window agent's current element queue is the element queue at the top of its custom element reactions stack.
|
||||
Vector<GC::Root<DOM::Element>>& current_element_queue() { return custom_element_reactions_stack.element_queue_stack.last(); }
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 1 tests
|
||||
|
||||
1 Pass
|
||||
Pass slotchange must fire on initialization of custom elements with slotted children
|
|
@ -0,0 +1,38 @@
|
|||
Harness status: OK
|
||||
|
||||
Found 32 tests
|
||||
|
||||
12 Pass
|
||||
20 Fail
|
||||
Fail slotchange event must fire on a default slot element inside an open shadow root in a document
|
||||
Fail slotchange event must fire on a default slot element inside a closed shadow root in a document
|
||||
Fail slotchange event must fire on a default slot element inside an open shadow root not in a document
|
||||
Fail slotchange event must fire on a default slot element inside a closed shadow root not in a document
|
||||
Fail slotchange event must fire on a named slot element insidean open shadow root in a document
|
||||
Fail slotchange event must fire on a named slot element insidea closed shadow root in a document
|
||||
Fail slotchange event must fire on a named slot element insidean open shadow root not in a document
|
||||
Fail slotchange event must fire on a named slot element insidea closed shadow root not in a document
|
||||
Pass slotchange event must not fire on a slot element inside an open shadow root in a document when another slot's assigned nodes change
|
||||
Pass slotchange event must not fire on a slot element inside a closed shadow root in a document when another slot's assigned nodes change
|
||||
Pass slotchange event must not fire on a slot element inside an open shadow root not in a document when another slot's assigned nodes change
|
||||
Pass slotchange event must not fire on a slot element inside a closed shadow root not in a document when another slot's assigned nodes change
|
||||
Pass slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside an open shadow root in a document
|
||||
Pass slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside a closed shadow root in a document
|
||||
Pass slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside an open shadow root not in a document
|
||||
Pass slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted and must not fire when the shadow host was mutated after the slot was removed inside a closed shadow root not in a document
|
||||
Fail slotchange event must fire on a slot element inside an open shadow root in a document even if the slot was removed immediately after the assigned nodes were mutated
|
||||
Fail slotchange event must fire on a slot element inside a closed shadow root in a document even if the slot was removed immediately after the assigned nodes were mutated
|
||||
Fail slotchange event must fire on a slot element inside an open shadow root not in a document even if the slot was removed immediately after the assigned nodes were mutated
|
||||
Fail slotchange event must fire on a slot element inside a closed shadow root not in a document even if the slot was removed immediately after the assigned nodes were mutated
|
||||
Fail slotchange event must fire on a slot element inside an open shadow root in a document when innerHTML modifies the children of the shadow host
|
||||
Fail slotchange event must fire on a slot element inside a closed shadow root in a document when innerHTML modifies the children of the shadow host
|
||||
Fail slotchange event must fire on a slot element inside an open shadow root not in a document when innerHTML modifies the children of the shadow host
|
||||
Fail slotchange event must fire on a slot element inside a closed shadow root not in a document when innerHTML modifies the children of the shadow host
|
||||
Pass slotchange event must fire on a slot element inside an open shadow root in a document when nested slots's contents change
|
||||
Pass slotchange event must fire on a slot element inside a closed shadow root in a document when nested slots's contents change
|
||||
Pass slotchange event must fire on a slot element inside an open shadow root not in a document when nested slots's contents change
|
||||
Pass slotchange event must fire on a slot element inside a closed shadow root not in a document when nested slots's contents change
|
||||
Fail slotchange event must fire at the end of current microtask after mutation observers are invoked inside an open shadow root in a document when slots's contents change
|
||||
Fail slotchange event must fire at the end of current microtask after mutation observers are invoked inside a closed shadow root in a document when slots's contents change
|
||||
Fail slotchange event must fire at the end of current microtask after mutation observers are invoked inside an open shadow root not in a document when slots's contents change
|
||||
Fail slotchange event must fire at the end of current microtask after mutation observers are invoked inside a closed shadow root not in a document when slots's contents change
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Shadow DOM: slotchange customelements</title>
|
||||
<meta name="author" title="Surma" href="mailto:surma@google.com">
|
||||
<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change">
|
||||
<script src="../resources/testharness.js"></script>
|
||||
<script src="../resources/testharnessreport.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<slots-in-constructor id="constructor-upgrade"><div></div></slots-in-constructor>
|
||||
<slots-in-callback id="callback-upgrade"><div></div></slots-in-callback>
|
||||
<script>
|
||||
var calls = [];
|
||||
class SlotsInConstructor extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.innerHTML = '<slot></slot>';
|
||||
var slot = this.shadowRoot.children[0];
|
||||
slot.addEventListener('slotchange', function() {
|
||||
calls.push(this.id);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
customElements.define('slots-in-constructor', SlotsInConstructor);
|
||||
class SlotsInCallback extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.innerHTML = '<slot></slot>';
|
||||
var slot = this.shadowRoot.children[0];
|
||||
slot.addEventListener('slotchange', function() {
|
||||
calls.push(this.id);
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
customElements.define('slots-in-callback', SlotsInCallback);
|
||||
</script>
|
||||
<slots-in-constructor id="constructor-parser"><div></div></slots-in-constructor>
|
||||
<slots-in-callback id="callback-parser"><div></div></slots-in-callback>
|
||||
<script>
|
||||
test(function () {
|
||||
assert_true(calls.includes("constructor-parser"));
|
||||
assert_true(calls.includes("callback-parser"));
|
||||
assert_true(calls.includes("constructor-upgrade"));
|
||||
assert_true(calls.includes("callback-upgrade"));
|
||||
}, 'slotchange must fire on initialization of custom elements with slotted children');
|
||||
done();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,602 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Shadow DOM: slotchange event</title>
|
||||
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
|
||||
<meta name="author" title="Hayato Ito" href="mailto:hayato@google.com">
|
||||
<link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change">
|
||||
<script src="../resources/testharness.js"></script>
|
||||
<script src="../resources/testharnessreport.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="log"></div>
|
||||
<script>
|
||||
function treeName(mode, connectedToDocument)
|
||||
{
|
||||
return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root '
|
||||
+ (connectedToDocument ? '' : ' not') + ' in a document';
|
||||
}
|
||||
|
||||
function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a default slot element inside '
|
||||
+ treeName(mode, connectedToDocument));
|
||||
|
||||
var host;
|
||||
var slot;
|
||||
var eventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
slot = document.createElement('slot');
|
||||
|
||||
slot.addEventListener('slotchange', function (event) {
|
||||
if (event.isFakeEvent)
|
||||
return;
|
||||
|
||||
test.step(function () {
|
||||
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
|
||||
assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
|
||||
assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
|
||||
});
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(slot);
|
||||
|
||||
host.appendChild(document.createElement('span'));
|
||||
host.appendChild(document.createElement('b'));
|
||||
|
||||
assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
|
||||
|
||||
host.appendChild(document.createElement('i'));
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
|
||||
|
||||
host.appendChild(document.createTextNode('hello'));
|
||||
|
||||
var fakeEvent = new Event('slotchange');
|
||||
fakeEvent.isFakeEvent = true;
|
||||
slot.dispatchEvent(fakeEvent);
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
|
||||
+ ' event if there was a synthetic slotchange event fired');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
}, 1);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testAppendingSpanToShadowRootWithDefaultSlot('open', true);
|
||||
testAppendingSpanToShadowRootWithDefaultSlot('closed', true);
|
||||
testAppendingSpanToShadowRootWithDefaultSlot('open', false);
|
||||
testAppendingSpanToShadowRootWithDefaultSlot('closed', false);
|
||||
|
||||
function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a named slot element inside'
|
||||
+ treeName(mode, connectedToDocument));
|
||||
|
||||
var host;
|
||||
var slot;
|
||||
var eventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
slot = document.createElement('slot');
|
||||
slot.name = 'someSlot';
|
||||
|
||||
slot.addEventListener('slotchange', function (event) {
|
||||
if (event.isFakeEvent)
|
||||
return;
|
||||
|
||||
test.step(function () {
|
||||
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
|
||||
assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
|
||||
assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
|
||||
});
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(slot);
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.slot = 'someSlot';
|
||||
host.appendChild(span);
|
||||
|
||||
var b = document.createElement('b');
|
||||
b.slot = 'someSlot';
|
||||
host.appendChild(b);
|
||||
|
||||
assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
|
||||
|
||||
var i = document.createElement('i');
|
||||
i.slot = 'someSlot';
|
||||
host.appendChild(i);
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
|
||||
|
||||
var em = document.createElement('em');
|
||||
em.slot = 'someSlot';
|
||||
host.appendChild(em);
|
||||
|
||||
var fakeEvent = new Event('slotchange');
|
||||
fakeEvent.isFakeEvent = true;
|
||||
slot.dispatchEvent(fakeEvent);
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
|
||||
+ ' event if there was a synthetic slotchange event fired');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
|
||||
}, 1);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testAppendingSpanToShadowRootWithNamedSlot('open', true);
|
||||
testAppendingSpanToShadowRootWithNamedSlot('closed', true);
|
||||
testAppendingSpanToShadowRootWithNamedSlot('open', false);
|
||||
testAppendingSpanToShadowRootWithNamedSlot('closed', false);
|
||||
|
||||
function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must not fire on a slot element inside '
|
||||
+ treeName(mode, connectedToDocument)
|
||||
+ ' when another slot\'s assigned nodes change');
|
||||
|
||||
var host;
|
||||
var defaultSlotEventCount = 0;
|
||||
var namedSlotEventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
var defaultSlot = document.createElement('slot');
|
||||
defaultSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
defaultSlotEventCount++;
|
||||
});
|
||||
|
||||
var namedSlot = document.createElement('slot');
|
||||
namedSlot.name = 'slotName';
|
||||
namedSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
namedSlotEventCount++;
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(defaultSlot);
|
||||
shadowRoot.appendChild(namedSlot);
|
||||
|
||||
host.appendChild(document.createElement('span'));
|
||||
|
||||
assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 1,
|
||||
'slotchange must be fired exactly once after the assigned nodes change on a default slot');
|
||||
assert_equals(namedSlotEventCount, 0,
|
||||
'slotchange must not be fired on a named slot after the assigned nodes change on a default slot');
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.slot = 'slotName';
|
||||
host.appendChild(span);
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 1,
|
||||
'slotchange must not be fired on a default slot after the assigned nodes change on a named slot');
|
||||
assert_equals(namedSlotEventCount, 1,
|
||||
'slotchange must be fired exactly once after the assigned nodes change on a default slot');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true);
|
||||
testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true);
|
||||
testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false);
|
||||
testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false);
|
||||
|
||||
function testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted'
|
||||
+ ' and must not fire when the shadow host was mutated after the slot was removed inside '
|
||||
+ treeName(mode, connectedToDocument));
|
||||
|
||||
var host;
|
||||
var slot;
|
||||
var eventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
slot = document.createElement('slot');
|
||||
slot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
host.appendChild(document.createElement('span'));
|
||||
shadowRoot.appendChild(slot);
|
||||
|
||||
assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 1,
|
||||
'slotchange must be fired on a slot element if there is assigned nodes when the slot was inserted');
|
||||
host.removeChild(host.firstChild);
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 2,
|
||||
'slotchange must be fired after the assigned nodes change on a slot while the slot element was in the tree');
|
||||
slot.parentNode.removeChild(slot);
|
||||
host.appendChild(document.createElement('span'));
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
assert_equals(eventCount, 2,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed');
|
||||
test.done();
|
||||
}, 1);
|
||||
}, 1);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', true);
|
||||
testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', true);
|
||||
testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', false);
|
||||
testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', false);
|
||||
|
||||
function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a slot element inside '
|
||||
+ treeName(mode, connectedToDocument)
|
||||
+ ' even if the slot was removed immediately after the assigned nodes were mutated');
|
||||
|
||||
var host;
|
||||
var slot;
|
||||
var anotherSlot;
|
||||
var slotEventCount = 0;
|
||||
var anotherSlotEventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
slot = document.createElement('slot');
|
||||
slot.name = 'someSlot';
|
||||
slot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
slotEventCount++;
|
||||
});
|
||||
|
||||
anotherSlot = document.createElement('slot');
|
||||
anotherSlot.name = 'someSlot';
|
||||
anotherSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
anotherSlotEventCount++;
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(slot);
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.slot = 'someSlot';
|
||||
host.appendChild(span);
|
||||
|
||||
shadowRoot.removeChild(slot);
|
||||
shadowRoot.appendChild(anotherSlot);
|
||||
|
||||
var span = document.createElement('span');
|
||||
span.slot = 'someSlot';
|
||||
host.appendChild(span);
|
||||
|
||||
shadowRoot.removeChild(anotherSlot);
|
||||
|
||||
assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(slotEventCount, 1,
|
||||
'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
|
||||
assert_equals(anotherSlotEventCount, 1,
|
||||
'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testSlotchangeFiresOnTransientlyPresentSlot('open', true);
|
||||
testSlotchangeFiresOnTransientlyPresentSlot('closed', true);
|
||||
testSlotchangeFiresOnTransientlyPresentSlot('open', false);
|
||||
testSlotchangeFiresOnTransientlyPresentSlot('closed', false);
|
||||
|
||||
function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a slot element inside '
|
||||
+ treeName(mode, connectedToDocument)
|
||||
+ ' when innerHTML modifies the children of the shadow host');
|
||||
|
||||
var host;
|
||||
var defaultSlot;
|
||||
var namedSlot;
|
||||
var defaultSlotEventCount = 0;
|
||||
var namedSlotEventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
defaultSlot = document.createElement('slot');
|
||||
defaultSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
defaultSlotEventCount++;
|
||||
});
|
||||
|
||||
namedSlot = document.createElement('slot');
|
||||
namedSlot.name = 'someSlot';
|
||||
namedSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
namedSlotEventCount++;
|
||||
});
|
||||
shadowRoot.appendChild(namedSlot);
|
||||
shadowRoot.appendChild(defaultSlot);
|
||||
host.innerHTML = 'foo <b>bar</b>';
|
||||
|
||||
assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 1,
|
||||
'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
|
||||
assert_equals(namedSlotEventCount, 0,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
|
||||
host.innerHTML = 'baz';
|
||||
});
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 2,
|
||||
'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
|
||||
assert_equals(namedSlotEventCount, 0,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
|
||||
host.innerHTML = '';
|
||||
});
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 3,
|
||||
'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
|
||||
assert_equals(namedSlotEventCount, 0,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
|
||||
host.innerHTML = '<b slot="someSlot">content</b>';
|
||||
});
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(defaultSlotEventCount, 3,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
|
||||
assert_equals(namedSlotEventCount, 1,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
|
||||
host.innerHTML = '';
|
||||
});
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
// FIXME: This test would fail in the current implementation because we can't tell
|
||||
// whether a text node was removed in AllChildrenRemoved or not.
|
||||
// assert_equals(defaultSlotEventCount, 3,
|
||||
// 'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
|
||||
assert_equals(namedSlotEventCount, 2,
|
||||
'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
}, 1);
|
||||
}, 1);
|
||||
}, 1);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testSlotchangeFiresOnInnerHTML('open', true);
|
||||
testSlotchangeFiresOnInnerHTML('closed', true);
|
||||
testSlotchangeFiresOnInnerHTML('open', false);
|
||||
testSlotchangeFiresOnInnerHTML('closed', false);
|
||||
|
||||
function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire on a slot element inside '
|
||||
+ treeName(mode, connectedToDocument)
|
||||
+ ' when nested slots\'s contents change');
|
||||
|
||||
var outerHost;
|
||||
var innerHost;
|
||||
var outerSlot;
|
||||
var innerSlot;
|
||||
var outerSlotEventCount = 0;
|
||||
var innerSlotEventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
outerHost = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(outerHost);
|
||||
|
||||
var outerShadow = outerHost.attachShadow({'mode': mode});
|
||||
outerShadow.appendChild(document.createElement('span'));
|
||||
outerSlot = document.createElement('slot');
|
||||
outerSlot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
outerSlotEventCount++;
|
||||
});
|
||||
|
||||
innerHost = document.createElement('div');
|
||||
innerHost.appendChild(outerSlot);
|
||||
outerShadow.appendChild(innerHost);
|
||||
|
||||
var innerShadow = innerHost.attachShadow({'mode': mode});
|
||||
innerShadow.appendChild(document.createElement('span'));
|
||||
innerSlot = document.createElement('slot');
|
||||
innerSlot.addEventListener('slotchange', function (event) {
|
||||
event.stopPropagation();
|
||||
test.step(function () {
|
||||
if (innerSlotEventCount === 0) {
|
||||
assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the inner slot element at 1st slotchange');
|
||||
} else if (innerSlotEventCount === 1) {
|
||||
assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the outer slot element at 2nd sltochange');
|
||||
}
|
||||
});
|
||||
innerSlotEventCount++;
|
||||
});
|
||||
innerShadow.appendChild(innerSlot);
|
||||
|
||||
outerHost.appendChild(document.createElement('span'));
|
||||
|
||||
assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(outerSlotEventCount, 1,
|
||||
'slotchange must be fired on a slot element if the assigned nodes changed');
|
||||
assert_equals(innerSlotEventCount, 2,
|
||||
'slotchange must be fired on a slot element and must bubble');
|
||||
});
|
||||
test.done();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
testSlotchangeFiresWhenNestedSlotChange('open', true);
|
||||
testSlotchangeFiresWhenNestedSlotChange('closed', true);
|
||||
testSlotchangeFiresWhenNestedSlotChange('open', false);
|
||||
testSlotchangeFiresWhenNestedSlotChange('closed', false);
|
||||
|
||||
function testSlotchangeFiresAtEndOfMicroTask(mode, connectedToDocument)
|
||||
{
|
||||
var test = async_test('slotchange event must fire at the end of current microtask after mutation observers are invoked inside '
|
||||
+ treeName(mode, connectedToDocument) + ' when slots\'s contents change');
|
||||
|
||||
var host;
|
||||
var slot;
|
||||
var eventCount = 0;
|
||||
|
||||
test.step(function () {
|
||||
host = document.createElement('div');
|
||||
if (connectedToDocument)
|
||||
document.body.appendChild(host);
|
||||
|
||||
var shadowRoot = host.attachShadow({'mode': mode});
|
||||
slot = document.createElement('slot');
|
||||
|
||||
slot.addEventListener('slotchange', function (event) {
|
||||
test.step(function () {
|
||||
assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
|
||||
assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
|
||||
});
|
||||
eventCount++;
|
||||
});
|
||||
|
||||
shadowRoot.appendChild(slot);
|
||||
});
|
||||
|
||||
var element = document.createElement('div');
|
||||
|
||||
new MutationObserver(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 0, 'slotchange event must not be fired before mutation records are delivered');
|
||||
});
|
||||
host.appendChild(document.createElement('span'));
|
||||
element.setAttribute('title', 'bar');
|
||||
}).observe(element, {attributes: true, attributeFilter: ['id']});
|
||||
|
||||
new MutationObserver(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 1, 'slotchange event must be fired during a single compound microtask');
|
||||
});
|
||||
}).observe(element, {attributes: true, attributeFilter: ['title']});
|
||||
|
||||
element.setAttribute('id', 'foo');
|
||||
host.appendChild(document.createElement('div'));
|
||||
|
||||
setTimeout(function () {
|
||||
test.step(function () {
|
||||
assert_equals(eventCount, 2,
|
||||
'a distinct slotchange event must be enqueued for changes made during a mutation observer delivery');
|
||||
});
|
||||
test.done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
testSlotchangeFiresAtEndOfMicroTask('open', true);
|
||||
testSlotchangeFiresAtEndOfMicroTask('closed', true);
|
||||
testSlotchangeFiresAtEndOfMicroTask('open', false);
|
||||
testSlotchangeFiresAtEndOfMicroTask('closed', false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue