tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

slotchange-event.html (24032B)


      1 <!DOCTYPE html>
      2 <html>
      3 <head>
      4 <title>Shadow DOM: slotchange event</title>
      5 <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
      6 <meta name="author" title="Hayato Ito" href="mailto:hayato@google.com">
      7 <link rel="help" href="https://dom.spec.whatwg.org/#signaling-slot-change">
      8 <script src="/resources/testharness.js"></script>
      9 <script src="/resources/testharnessreport.js"></script>
     10 </head>
     11 <body>
     12 <div id="log"></div>
     13 <script>
     14 function treeName(mode, connectedToDocument)
     15 {
     16    return (mode == 'open' ? 'an ' : 'a ') + mode + ' shadow root '
     17        + (connectedToDocument ? '' : ' not') + ' in a document';
     18 }
     19 
     20 function testAppendingSpanToShadowRootWithDefaultSlot(mode, connectedToDocument)
     21 {
     22    var test = async_test('slotchange event must fire on a default slot element inside '
     23        + treeName(mode, connectedToDocument));
     24 
     25    var host;
     26    var slot;
     27    var eventCount = 0;
     28 
     29    test.step(function () {
     30        host = document.createElement('div');
     31        if (connectedToDocument)
     32            document.body.appendChild(host);
     33 
     34        var shadowRoot = host.attachShadow({'mode': mode});
     35        slot = document.createElement('slot');
     36 
     37        slot.addEventListener('slotchange', function (event) {
     38            if (event.isFakeEvent)
     39                return;
     40 
     41            test.step(function () {
     42                assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
     43                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
     44                assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
     45            });
     46            eventCount++;
     47        });
     48 
     49        shadowRoot.appendChild(slot);
     50 
     51        host.appendChild(document.createElement('span'));
     52        host.appendChild(document.createElement('b'));
     53 
     54        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
     55    });
     56 
     57    setTimeout(function () {
     58        test.step(function () {
     59            assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
     60 
     61            host.appendChild(document.createElement('i'));
     62        });
     63 
     64        setTimeout(function () {
     65            test.step(function () {
     66                assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
     67 
     68                host.appendChild(document.createTextNode('hello'));
     69 
     70                var fakeEvent = new Event('slotchange');
     71                fakeEvent.isFakeEvent = true;
     72                slot.dispatchEvent(fakeEvent);
     73            });
     74 
     75            setTimeout(function () {
     76                test.step(function () {
     77                    assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
     78                        + ' event if there was a synthetic slotchange event fired');
     79                });
     80                test.done();
     81            }, 1);
     82        }, 1);
     83    }, 1);
     84 }
     85 
     86 testAppendingSpanToShadowRootWithDefaultSlot('open', true);
     87 testAppendingSpanToShadowRootWithDefaultSlot('closed', true);
     88 testAppendingSpanToShadowRootWithDefaultSlot('open', false);
     89 testAppendingSpanToShadowRootWithDefaultSlot('closed', false);
     90 
     91 function testAppendingSpanToShadowRootWithNamedSlot(mode, connectedToDocument)
     92 {
     93    var test = async_test('slotchange event must fire on a named slot element inside'
     94        + treeName(mode, connectedToDocument));
     95 
     96    var host;
     97    var slot;
     98    var eventCount = 0;
     99 
    100    test.step(function () {
    101        host = document.createElement('div');
    102        if (connectedToDocument)
    103            document.body.appendChild(host);
    104 
    105        var shadowRoot = host.attachShadow({'mode': mode});
    106        slot = document.createElement('slot');
    107        slot.name = 'someSlot';
    108 
    109        slot.addEventListener('slotchange', function (event) {
    110            if (event.isFakeEvent)
    111                return;
    112 
    113            test.step(function () {
    114                assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
    115                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
    116                assert_equals(event.relatedTarget, undefined, 'slotchange must not set relatedTarget');
    117            });
    118            eventCount++;
    119        });
    120 
    121        shadowRoot.appendChild(slot);
    122 
    123        var span = document.createElement('span');
    124        span.slot = 'someSlot';
    125        host.appendChild(span);
    126 
    127        var b = document.createElement('b');
    128        b.slot = 'someSlot';
    129        host.appendChild(b);
    130 
    131        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
    132    });
    133 
    134    setTimeout(function () {
    135        test.step(function () {
    136            assert_equals(eventCount, 1, 'slotchange must be fired exactly once after the assigned nodes changed');
    137 
    138            var i = document.createElement('i');
    139            i.slot = 'someSlot';
    140            host.appendChild(i);
    141        });
    142 
    143        setTimeout(function () {
    144            test.step(function () {
    145                assert_equals(eventCount, 2, 'slotchange must be fired exactly once after the assigned nodes changed');
    146 
    147                var em = document.createElement('em');
    148                em.slot = 'someSlot';
    149                host.appendChild(em);
    150 
    151                var fakeEvent = new Event('slotchange');
    152                fakeEvent.isFakeEvent = true;
    153                slot.dispatchEvent(fakeEvent);
    154            });
    155 
    156            setTimeout(function () {
    157                test.step(function () {
    158                    assert_equals(eventCount, 3, 'slotchange must be fired exactly once after the assigned nodes changed'
    159                        + ' event if there was a synthetic slotchange event fired');
    160                });
    161                test.done();
    162            }, 1);
    163 
    164        }, 1);
    165    }, 1);
    166 }
    167 
    168 testAppendingSpanToShadowRootWithNamedSlot('open', true);
    169 testAppendingSpanToShadowRootWithNamedSlot('closed', true);
    170 testAppendingSpanToShadowRootWithNamedSlot('open', false);
    171 testAppendingSpanToShadowRootWithNamedSlot('closed', false);
    172 
    173 function testSlotchangeDoesNotFireWhenOtherSlotsChange(mode, connectedToDocument)
    174 {
    175    var test = async_test('slotchange event must not fire on a slot element inside '
    176        + treeName(mode, connectedToDocument)
    177        + ' when another slot\'s assigned nodes change');
    178 
    179    var host;
    180    var defaultSlotEventCount = 0;
    181    var namedSlotEventCount = 0;
    182 
    183    test.step(function () {
    184        host = document.createElement('div');
    185        if (connectedToDocument)
    186            document.body.appendChild(host);
    187 
    188        var shadowRoot = host.attachShadow({'mode': mode});
    189        var defaultSlot = document.createElement('slot');
    190        defaultSlot.addEventListener('slotchange', function (event) {
    191            test.step(function () {
    192                assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
    193            });
    194            defaultSlotEventCount++;
    195        });
    196 
    197        var namedSlot = document.createElement('slot');
    198        namedSlot.name = 'slotName';
    199        namedSlot.addEventListener('slotchange', function (event) {
    200            test.step(function () {
    201                assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
    202            });
    203            namedSlotEventCount++;
    204        });
    205 
    206        shadowRoot.appendChild(defaultSlot);
    207        shadowRoot.appendChild(namedSlot);
    208 
    209        host.appendChild(document.createElement('span'));
    210 
    211        assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    212        assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    213    });
    214 
    215    setTimeout(function () {
    216        test.step(function () {
    217            assert_equals(defaultSlotEventCount, 1,
    218                'slotchange must be fired exactly once after the assigned nodes change on a default slot');
    219            assert_equals(namedSlotEventCount, 0,
    220                'slotchange must not be fired on a named slot after the assigned nodes change on a default slot');
    221 
    222            var span = document.createElement('span');
    223            span.slot = 'slotName';
    224            host.appendChild(span);
    225        });
    226 
    227        setTimeout(function () {
    228            test.step(function () {
    229                assert_equals(defaultSlotEventCount, 1,
    230                    'slotchange must not be fired on a default slot after the assigned nodes change on a named slot');
    231                assert_equals(namedSlotEventCount, 1,
    232                    'slotchange must be fired exactly once after the assigned nodes change on a default slot');
    233            });
    234            test.done();
    235        }, 1);
    236    }, 1);
    237 }
    238 
    239 testSlotchangeDoesNotFireWhenOtherSlotsChange('open', true);
    240 testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', true);
    241 testSlotchangeDoesNotFireWhenOtherSlotsChange('open', false);
    242 testSlotchangeDoesNotFireWhenOtherSlotsChange('closed', false);
    243 
    244 function testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved(mode, connectedToDocument)
    245 {
    246    var test = async_test('slotchange event must fire on a slot element when a shadow host has a slottable and the slot was inserted'
    247                        + ' and must not fire when the shadow host was mutated after the slot was removed inside '
    248                        + treeName(mode, connectedToDocument));
    249 
    250    var host;
    251    var slot;
    252    var eventCount = 0;
    253 
    254    test.step(function () {
    255        host = document.createElement('div');
    256        if (connectedToDocument)
    257            document.body.appendChild(host);
    258 
    259        var shadowRoot = host.attachShadow({'mode': mode});
    260        slot = document.createElement('slot');
    261        slot.addEventListener('slotchange', function (event) {
    262            test.step(function () {
    263                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
    264            });
    265            eventCount++;
    266        });
    267 
    268        host.appendChild(document.createElement('span'));
    269        shadowRoot.appendChild(slot);
    270 
    271        assert_equals(eventCount, 0, 'slotchange event must not be fired synchronously');
    272    });
    273 
    274    setTimeout(function () {
    275        test.step(function () {
    276            assert_equals(eventCount, 1,
    277                          'slotchange must be fired on a slot element if there is assigned nodes when the slot was inserted');
    278            host.removeChild(host.firstChild);
    279        });
    280 
    281        setTimeout(function () {
    282            test.step(function () {
    283                assert_equals(eventCount, 2,
    284                              'slotchange must be fired after the assigned nodes change on a slot while the slot element was in the tree');
    285                slot.parentNode.removeChild(slot);
    286                host.appendChild(document.createElement('span'));
    287            });
    288 
    289            setTimeout(function () {
    290                assert_equals(eventCount, 2,
    291                              'slotchange must not be fired on a slot element if the assigned nodes changed after the slot was removed');
    292                test.done();
    293            }, 1);
    294        }, 1);
    295    }, 1);
    296 }
    297 
    298 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', true);
    299 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', true);
    300 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('open', false);
    301 testSlotchangeDoesFireAtInsertedAndDoesNotFireForMutationAfterRemoved('closed', false);
    302 
    303 function testSlotchangeFiresOnTransientlyPresentSlot(mode, connectedToDocument)
    304 {
    305    var test = async_test('slotchange event must fire on a slot element inside '
    306        + treeName(mode, connectedToDocument)
    307        + ' even if the slot was removed immediately after the assigned nodes were mutated');
    308 
    309    var host;
    310    var slot;
    311    var anotherSlot;
    312    var slotEventCount = 0;
    313    var anotherSlotEventCount = 0;
    314 
    315    test.step(function () {
    316        host = document.createElement('div');
    317        if (connectedToDocument)
    318            document.body.appendChild(host);
    319 
    320        var shadowRoot = host.attachShadow({'mode': mode});
    321        slot = document.createElement('slot');
    322        slot.name = 'someSlot';
    323        slot.addEventListener('slotchange', function (event) {
    324            test.step(function () {
    325                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
    326            });
    327            slotEventCount++;
    328        });
    329 
    330        anotherSlot = document.createElement('slot');
    331        anotherSlot.name = 'someSlot';
    332        anotherSlot.addEventListener('slotchange', function (event) {
    333            test.step(function () {
    334                assert_equals(event.target, anotherSlot, 'slotchange event\'s target must be the slot element');
    335            });
    336            anotherSlotEventCount++;
    337        });
    338 
    339        shadowRoot.appendChild(slot);
    340 
    341        var span = document.createElement('span');
    342        span.slot = 'someSlot';
    343        host.appendChild(span);
    344 
    345        shadowRoot.removeChild(slot);
    346        shadowRoot.appendChild(anotherSlot);
    347 
    348        var span = document.createElement('span');
    349        span.slot = 'someSlot';
    350        host.appendChild(span);
    351 
    352        shadowRoot.removeChild(anotherSlot);
    353 
    354        assert_equals(slotEventCount, 0, 'slotchange event must not be fired synchronously');
    355        assert_equals(anotherSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    356    });
    357 
    358    setTimeout(function () {
    359        test.step(function () {
    360            assert_equals(slotEventCount, 1,
    361                'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
    362            assert_equals(anotherSlotEventCount, 1,
    363                'slotchange must be fired on a slot element if the assigned nodes changed while the slot was present');
    364        });
    365        test.done();
    366    }, 1);
    367 }
    368 
    369 testSlotchangeFiresOnTransientlyPresentSlot('open', true);
    370 testSlotchangeFiresOnTransientlyPresentSlot('closed', true);
    371 testSlotchangeFiresOnTransientlyPresentSlot('open', false);
    372 testSlotchangeFiresOnTransientlyPresentSlot('closed', false);
    373 
    374 function testSlotchangeFiresOnInnerHTML(mode, connectedToDocument)
    375 {
    376    var test = async_test('slotchange event must fire on a slot element inside '
    377        + treeName(mode, connectedToDocument)
    378        + ' when innerHTML modifies the children of the shadow host');
    379 
    380    var host;
    381    var defaultSlot;
    382    var namedSlot;
    383    var defaultSlotEventCount = 0;
    384    var namedSlotEventCount = 0;
    385 
    386    test.step(function () {
    387        host = document.createElement('div');
    388        if (connectedToDocument)
    389            document.body.appendChild(host);
    390 
    391        var shadowRoot = host.attachShadow({'mode': mode});
    392        defaultSlot = document.createElement('slot');
    393        defaultSlot.addEventListener('slotchange', function (event) {
    394            test.step(function () {
    395                assert_equals(event.target, defaultSlot, 'slotchange event\'s target must be the slot element');
    396            });
    397            defaultSlotEventCount++;
    398        });
    399 
    400        namedSlot = document.createElement('slot');
    401        namedSlot.name = 'someSlot';
    402        namedSlot.addEventListener('slotchange', function (event) {
    403            test.step(function () {
    404                assert_equals(event.target, namedSlot, 'slotchange event\'s target must be the slot element');
    405            });
    406            namedSlotEventCount++;
    407        });
    408        shadowRoot.appendChild(namedSlot);
    409        shadowRoot.appendChild(defaultSlot);
    410        host.innerHTML = 'foo <b>bar</b>';
    411 
    412        assert_equals(defaultSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    413        assert_equals(namedSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    414    });
    415 
    416    setTimeout(function () {
    417        test.step(function () {
    418            assert_equals(defaultSlotEventCount, 1,
    419                'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
    420            assert_equals(namedSlotEventCount, 0,
    421                'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
    422            host.innerHTML = 'baz';
    423        });
    424        setTimeout(function () {
    425            test.step(function () {
    426                assert_equals(defaultSlotEventCount, 2,
    427                    'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
    428                assert_equals(namedSlotEventCount, 0,
    429                    'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
    430                host.innerHTML = '';
    431            });
    432            setTimeout(function () {
    433                test.step(function () {
    434                    assert_equals(defaultSlotEventCount, 3,
    435                        'slotchange must be fired on a slot element if the assigned nodes are changed by innerHTML');
    436                    assert_equals(namedSlotEventCount, 0,
    437                        'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
    438                    host.innerHTML = '<b slot="someSlot">content</b>';
    439                });
    440                setTimeout(function () {
    441                    test.step(function () {
    442                        assert_equals(defaultSlotEventCount, 3,
    443                            'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
    444                        assert_equals(namedSlotEventCount, 1,
    445                            'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
    446                        host.innerHTML = '';
    447                    });
    448                    setTimeout(function () {
    449                        test.step(function () {
    450                            // FIXME: This test would fail in the current implementation because we can't tell
    451                            // whether a text node was removed in AllChildrenRemoved or not.
    452 //                            assert_equals(defaultSlotEventCount, 3,
    453 //                                'slotchange must not be fired on a slot element if the assigned nodes are not changed by innerHTML');
    454                            assert_equals(namedSlotEventCount, 2,
    455                                'slotchange must not be fired on a slot element if the assigned nodes are changed by innerHTML');
    456                        });
    457                        test.done();
    458                    }, 1);
    459                }, 1);
    460            }, 1);
    461        }, 1);
    462    }, 1);
    463 }
    464 
    465 testSlotchangeFiresOnInnerHTML('open', true);
    466 testSlotchangeFiresOnInnerHTML('closed', true);
    467 testSlotchangeFiresOnInnerHTML('open', false);
    468 testSlotchangeFiresOnInnerHTML('closed', false);
    469 
    470 function testSlotchangeFiresWhenNestedSlotChange(mode, connectedToDocument)
    471 {
    472    var test = async_test('slotchange event must fire on a slot element inside '
    473        + treeName(mode, connectedToDocument)
    474        + ' when nested slots\'s contents change');
    475 
    476    var outerHost;
    477    var innerHost;
    478    var outerSlot;
    479    var innerSlot;
    480    var outerSlotEventCount = 0;
    481    var innerSlotEventCount = 0;
    482 
    483    test.step(function () {
    484        outerHost = document.createElement('div');
    485        if (connectedToDocument)
    486            document.body.appendChild(outerHost);
    487 
    488        var outerShadow = outerHost.attachShadow({'mode': mode});
    489        outerShadow.appendChild(document.createElement('span'));
    490        outerSlot = document.createElement('slot');
    491        outerSlot.addEventListener('slotchange', function (event) {
    492            test.step(function () {
    493                assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the slot element');
    494            });
    495            outerSlotEventCount++;
    496        });
    497 
    498        innerHost = document.createElement('div');
    499        innerHost.appendChild(outerSlot);
    500        outerShadow.appendChild(innerHost);
    501 
    502        var innerShadow = innerHost.attachShadow({'mode': mode});
    503        innerShadow.appendChild(document.createElement('span'));
    504        innerSlot = document.createElement('slot');
    505        innerSlot.addEventListener('slotchange', function (event) {
    506            event.stopPropagation();
    507            test.step(function () {
    508                if (innerSlotEventCount === 0) {
    509                    assert_equals(event.target, innerSlot, 'slotchange event\'s target must be the inner slot element at 1st slotchange');
    510                } else if (innerSlotEventCount === 1) {
    511                    assert_equals(event.target, outerSlot, 'slotchange event\'s target must be the outer slot element at 2nd sltochange');
    512                }
    513            });
    514            innerSlotEventCount++;
    515        });
    516        innerShadow.appendChild(innerSlot);
    517 
    518        outerHost.appendChild(document.createElement('span'));
    519 
    520        assert_equals(innerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    521        assert_equals(outerSlotEventCount, 0, 'slotchange event must not be fired synchronously');
    522    });
    523 
    524    setTimeout(function () {
    525        test.step(function () {
    526            assert_equals(outerSlotEventCount, 1,
    527                'slotchange must be fired on a slot element if the assigned nodes changed');
    528            assert_equals(innerSlotEventCount, 2,
    529                'slotchange must be fired on a slot element and must bubble');
    530        });
    531        test.done();
    532    }, 1);
    533 }
    534 
    535 testSlotchangeFiresWhenNestedSlotChange('open', true);
    536 testSlotchangeFiresWhenNestedSlotChange('closed', true);
    537 testSlotchangeFiresWhenNestedSlotChange('open', false);
    538 testSlotchangeFiresWhenNestedSlotChange('closed', false);
    539 
    540 function testSlotchangeFiresAtEndOfMicroTask(mode, connectedToDocument)
    541 {
    542    var test = async_test('slotchange event must fire at the end of current microtask after mutation observers are invoked inside '
    543        + treeName(mode, connectedToDocument) + ' when slots\'s contents change');
    544 
    545    var host;
    546    var slot;
    547    var eventCount = 0;
    548 
    549    test.step(function () {
    550        host = document.createElement('div');
    551        if (connectedToDocument)
    552            document.body.appendChild(host);
    553 
    554        var shadowRoot = host.attachShadow({'mode': mode});
    555        slot = document.createElement('slot');
    556 
    557        slot.addEventListener('slotchange', function (event) {
    558            test.step(function () {
    559                assert_equals(event.type, 'slotchange', 'slotchange event\'s type must be "slotchange"');
    560                assert_equals(event.target, slot, 'slotchange event\'s target must be the slot element');
    561            });
    562            eventCount++;
    563        });
    564 
    565        shadowRoot.appendChild(slot);
    566    });
    567 
    568    var element = document.createElement('div');
    569 
    570    new MutationObserver(function () {
    571        test.step(function () {
    572            assert_equals(eventCount, 0, 'slotchange event must not be fired before mutation records are delivered');
    573        });
    574        host.appendChild(document.createElement('span'));
    575        element.setAttribute('title', 'bar');
    576    }).observe(element, {attributes: true, attributeFilter: ['id']});
    577 
    578    new MutationObserver(function () {
    579        test.step(function () {
    580            assert_equals(eventCount, 1, 'slotchange event must be fired during a single compound microtask');
    581        });
    582    }).observe(element, {attributes: true, attributeFilter: ['title']});
    583 
    584    element.setAttribute('id', 'foo');
    585    host.appendChild(document.createElement('div'));
    586 
    587    setTimeout(function () {
    588        test.step(function () {
    589            assert_equals(eventCount, 2,
    590                'a distinct slotchange event must be enqueued for changes made during a mutation observer delivery');
    591        });
    592        test.done();
    593    }, 0);
    594 }
    595 
    596 testSlotchangeFiresAtEndOfMicroTask('open', true);
    597 testSlotchangeFiresAtEndOfMicroTask('closed', true);
    598 testSlotchangeFiresAtEndOfMicroTask('open', false);
    599 testSlotchangeFiresAtEndOfMicroTask('closed', false);
    600 </script>
    601 </body>
    602 </html>