CustomElementRegistry.html (40855B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>Custom Elements: CustomElementRegistry interface</title> 5 <meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org"> 6 <meta name="assert" content="CustomElementRegistry interface must exist"> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 </head> 10 <body> 11 <div id="log"></div> 12 <script> 13 14 const moveBefore_supported = ("moveBefore" in Element.prototype); 15 16 test(function () { 17 assert_true('define' in CustomElementRegistry.prototype, '"define" exists on CustomElementRegistry.prototype'); 18 assert_true('define' in customElements, '"define" exists on window.customElements'); 19 }, 'CustomElementRegistry interface must have define as a method'); 20 21 test(function () { 22 assert_throws_js(TypeError, function () { customElements.define('badname', 1); }, 23 'customElements.define must throw a TypeError when the element interface is a number'); 24 assert_throws_js(TypeError, function () { customElements.define('badname', '123'); }, 25 'customElements.define must throw a TypeError when the element interface is a string'); 26 assert_throws_js(TypeError, function () { customElements.define('badname', {}); }, 27 'customElements.define must throw a TypeError when the element interface is an object'); 28 assert_throws_js(TypeError, function () { customElements.define('badname', []); }, 29 'customElements.define must throw a TypeError when the element interface is an array'); 30 }, 'customElements.define must throw when the element interface is not a constructor'); 31 32 test(function () { 33 customElements.define('custom-html-element', HTMLElement); 34 }, 'customElements.define must not throw the constructor is HTMLElement'); 35 36 test(function () { 37 class MyCustomElement extends HTMLElement {}; 38 39 assert_throws_dom('SyntaxError', function () { customElements.define(null, MyCustomElement); }, 40 'customElements.define must throw a SyntaxError if the tag name is null'); 41 assert_throws_dom('SyntaxError', function () { customElements.define('', MyCustomElement); }, 42 'customElements.define must throw a SyntaxError if the tag name is empty'); 43 assert_throws_dom('SyntaxError', function () { customElements.define('abc', MyCustomElement); }, 44 'customElements.define must throw a SyntaxError if the tag name does not contain "-"'); 45 assert_throws_dom('SyntaxError', function () { customElements.define('a-Bc', MyCustomElement); }, 46 'customElements.define must throw a SyntaxError if the tag name contains an upper case letter'); 47 48 var builtinTagNames = [ 49 'annotation-xml', 50 'color-profile', 51 'font-face', 52 'font-face-src', 53 'font-face-uri', 54 'font-face-format', 55 'font-face-name', 56 'missing-glyph' 57 ]; 58 59 for (var tagName of builtinTagNames) { 60 assert_throws_dom('SyntaxError', function () { customElements.define(tagName, MyCustomElement); }, 61 'customElements.define must throw a SyntaxError if the tag name is "' + tagName + '"'); 62 } 63 64 }, 'customElements.define must throw with an invalid name'); 65 66 test(function () { 67 class SomeCustomElement extends HTMLElement {}; 68 69 var calls = []; 70 var OtherCustomElement = new Proxy(class extends HTMLElement {}, { 71 get: function (target, name) { 72 calls.push(name); 73 return target[name]; 74 } 75 }) 76 77 customElements.define('some-custom-element', SomeCustomElement); 78 assert_throws_dom('NotSupportedError', function () { customElements.define('some-custom-element', OtherCustomElement); }, 79 'customElements.define must throw a NotSupportedError if the specified tag name is already used'); 80 assert_array_equals(calls, [], 'customElements.define must validate the custom element name before getting the prototype of the constructor'); 81 82 }, 'customElements.define must throw when there is already a custom element of the same name'); 83 84 test(function () { 85 class AnotherCustomElement extends HTMLElement {}; 86 87 customElements.define('another-custom-element', AnotherCustomElement); 88 assert_throws_dom('NotSupportedError', function () { customElements.define('some-other-element', AnotherCustomElement); }, 89 'customElements.define must throw a NotSupportedError if the specified class already defines an element'); 90 91 }, 'customElements.define must throw a NotSupportedError when there is already a custom element with the same class'); 92 93 test(function () { 94 var outerCalls = []; 95 var OuterCustomElement = new Proxy(class extends HTMLElement { }, { 96 get: function (target, name) { 97 outerCalls.push(name); 98 customElements.define('inner-custom-element', InnerCustomElement); 99 return target[name]; 100 } 101 }); 102 var innerCalls = []; 103 var InnerCustomElement = new Proxy(class extends HTMLElement { }, { 104 get: function (target, name) { 105 outerCalls.push(name); 106 return target[name]; 107 } 108 }); 109 110 assert_throws_dom('NotSupportedError', function () { customElements.define('outer-custom-element', OuterCustomElement); }, 111 'customElements.define must throw a NotSupportedError if the specified class already defines an element'); 112 assert_array_equals(outerCalls, ['prototype'], 'customElements.define must get "prototype"'); 113 assert_array_equals(innerCalls, [], 114 'customElements.define must throw a NotSupportedError when element definition is running flag is set' 115 + ' before getting the prototype of the constructor'); 116 117 }, 'customElements.define must throw a NotSupportedError when element definition is running flag is set'); 118 119 test(function () { 120 var calls = []; 121 var ElementWithBadInnerConstructor = new Proxy(class extends HTMLElement { }, { 122 get: function (target, name) { 123 calls.push(name); 124 customElements.define('inner-custom-element', 1); 125 return target[name]; 126 } 127 }); 128 129 assert_throws_js(TypeError, function () { 130 customElements.define('element-with-bad-inner-constructor', ElementWithBadInnerConstructor); 131 }, 'customElements.define must throw a NotSupportedError if IsConstructor(constructor) is false'); 132 133 assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"'); 134 }, 'customElements.define must check IsConstructor on the constructor before checking the element definition is running flag'); 135 136 test(function () { 137 var calls = []; 138 var ElementWithBadInnerName = new Proxy(class extends HTMLElement { }, { 139 get: function (target, name) { 140 calls.push(name); 141 customElements.define('badname', class extends HTMLElement {}); 142 return target[name]; 143 } 144 }); 145 146 assert_throws_dom('SyntaxError', function () { 147 customElements.define('element-with-bad-inner-name', ElementWithBadInnerName); 148 }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name'); 149 150 assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"'); 151 }, 'customElements.define must validate the custom element name before checking the element definition is running flag'); 152 153 test(function () { 154 var unresolvedElement = document.createElement('constructor-calls-define'); 155 document.body.appendChild(unresolvedElement); 156 var elementUpgradedDuringUpgrade = document.createElement('defined-during-upgrade'); 157 document.body.appendChild(elementUpgradedDuringUpgrade); 158 159 var DefinedDuringUpgrade = class extends HTMLElement { }; 160 161 class ConstructorCallsDefine extends HTMLElement { 162 constructor() { 163 customElements.define('defined-during-upgrade', DefinedDuringUpgrade); 164 assert_false(unresolvedElement instanceof ConstructorCallsDefine); 165 assert_true(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade); 166 super(); 167 assert_true(unresolvedElement instanceof ConstructorCallsDefine); 168 assert_true(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade); 169 } 170 } 171 172 assert_false(unresolvedElement instanceof ConstructorCallsDefine); 173 assert_false(elementUpgradedDuringUpgrade instanceof DefinedDuringUpgrade); 174 175 customElements.define('constructor-calls-define', ConstructorCallsDefine); 176 }, 'customElements.define unset the element definition is running flag before upgrading custom elements'); 177 178 (function () { 179 var testCase = async_test('customElements.define must not throw' 180 +' when defining another custom element in a different global object during Get(constructor, "prototype")'); 181 182 var iframe = document.createElement('iframe'); 183 iframe.onload = function () { 184 testCase.step(function () { 185 var InnerCustomElement = class extends iframe.contentWindow.HTMLElement {}; 186 var calls = []; 187 var proxy = new Proxy(class extends HTMLElement { }, { 188 get: function (target, name) { 189 calls.push(name); 190 if (name === "prototype") { 191 iframe.contentWindow.customElements.define('another-custom-element', InnerCustomElement); 192 } 193 return target[name]; 194 } 195 }) 196 customElements.define('element-with-inner-element-define', proxy); 197 assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated'], 198 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" on the constructor'); 199 assert_true(iframe.contentDocument.createElement('another-custom-element') instanceof InnerCustomElement); 200 }); 201 document.body.removeChild(iframe); 202 testCase.done(); 203 } 204 205 document.body.appendChild(iframe); 206 })(); 207 208 test(function () { 209 var calls = []; 210 var ElementWithBadInnerName = new Proxy(class extends HTMLElement { }, { 211 get: function (target, name) { 212 calls.push(name); 213 customElements.define('badname', class extends HTMLElement {}); 214 return target[name]; 215 } 216 }); 217 218 assert_throws_dom('SyntaxError', function () { 219 customElements.define('element-with-bad-inner-name', ElementWithBadInnerName); 220 }, 'customElements.define must throw a SyntaxError if the specified name is not a valid custom element name'); 221 222 assert_array_equals(calls, ['prototype'], 'customElements.define must get "prototype"'); 223 }, ''); 224 225 test(function () { 226 var calls = []; 227 var proxy = new Proxy(class extends HTMLElement { }, { 228 get: function (target, name) { 229 calls.push(name); 230 return target[name]; 231 } 232 }); 233 customElements.define('proxy-element', proxy); 234 assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated']); 235 }, 'customElements.define must get "prototype", "disabledFeatures", and "formAssociated" property of the constructor'); 236 237 test(function () { 238 var err = {name: 'expectedError'} 239 var proxy = new Proxy(class extends HTMLElement { }, { 240 get: function (target, name) { 241 throw err; 242 } 243 }); 244 assert_throws_exactly(err, function () { customElements.define('element-with-string-prototype', proxy); }); 245 }, 'customElements.define must rethrow an exception thrown while getting "prototype" property of the constructor'); 246 247 test(function () { 248 var returnedValue; 249 var proxy = new Proxy(class extends HTMLElement { }, { 250 get: function (target, name) { return returnedValue; } 251 }); 252 253 returnedValue = null; 254 assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); }, 255 'customElements.define must throw when "prototype" property of the constructor is null'); 256 returnedValue = undefined; 257 assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); }, 258 'customElements.define must throw when "prototype" property of the constructor is undefined'); 259 returnedValue = 'hello'; 260 assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); }, 261 'customElements.define must throw when "prototype" property of the constructor is a string'); 262 returnedValue = 1; 263 assert_throws_js(TypeError, function () { customElements.define('element-with-string-prototype', proxy); }, 264 'customElements.define must throw when "prototype" property of the constructor is a number'); 265 266 }, 'customElements.define must throw when "prototype" property of the constructor is not an object'); 267 268 test(function () { 269 var constructor = function () {} 270 var calls = []; 271 constructor.prototype = new Proxy(constructor.prototype, { 272 get: function (target, name) { 273 calls.push(name); 274 return target[name]; 275 } 276 }); 277 customElements.define('element-with-proxy-prototype', constructor); 278 assert_array_equals(calls, 279 moveBefore_supported ? ['connectedCallback', 'disconnectedCallback', 'connectedMoveCallback', 'adoptedCallback', 'attributeChangedCallback'] : 280 ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback']); 281 }, 'customElements.define must get callbacks of the constructor prototype'); 282 283 test(function () { 284 var constructor = function () {} 285 var calls = []; 286 var err = {name: 'expectedError'} 287 constructor.prototype = new Proxy(constructor.prototype, { 288 get: function (target, name) { 289 calls.push(name); 290 if (name == 'disconnectedCallback') 291 throw err; 292 return target[name]; 293 } 294 }); 295 assert_throws_exactly(err, function () { customElements.define('element-with-throwing-callback', constructor); }); 296 assert_array_equals(calls, ['connectedCallback', 'disconnectedCallback'], 297 'customElements.define must not get callbacks after one of the get throws'); 298 }, 'customElements.define must rethrow an exception thrown while getting callbacks on the constructor prototype'); 299 300 test(function () { 301 var constructor = function () {} 302 var calls = []; 303 constructor.prototype = new Proxy(constructor.prototype, { 304 get: function (target, name) { 305 calls.push(name); 306 if (name == 'adoptedCallback') 307 return 1; 308 return target[name]; 309 } 310 }); 311 assert_throws_js(TypeError, function () { customElements.define('element-with-throwing-callback', constructor); }); 312 assert_array_equals(calls, 313 moveBefore_supported ? 314 ['connectedCallback', 'disconnectedCallback', 'connectedMoveCallback', 'adoptedCallback'] : 315 ['connectedCallback', 'disconnectedCallback', 'adoptedCallback'], 316 'customElements.define must not get callbacks after one of the conversion throws'); 317 }, 'customElements.define must rethrow an exception thrown while converting a callback value to Function callback type'); 318 319 test(function () { 320 var constructor = function () {} 321 constructor.prototype.attributeChangedCallback = function () { }; 322 var prototypeCalls = []; 323 var callOrder = 0; 324 constructor.prototype = new Proxy(constructor.prototype, { 325 get: function (target, name) { 326 if (name == 'prototype' || name == 'observedAttributes') 327 throw 'Unexpected access to observedAttributes'; 328 prototypeCalls.push(callOrder++); 329 prototypeCalls.push(name); 330 return target[name]; 331 } 332 }); 333 var constructorCalls = []; 334 var proxy = new Proxy(constructor, { 335 get: function (target, name) { 336 constructorCalls.push(callOrder++); 337 constructorCalls.push(name); 338 return target[name]; 339 } 340 }); 341 customElements.define('element-with-attribute-changed-callback', proxy); 342 assert_array_equals(prototypeCalls, 343 moveBefore_supported ? 344 [1, 'connectedCallback', 2, 'disconnectedCallback', 3, 'connectedMoveCallback', 4, 'adoptedCallback', 5, 'attributeChangedCallback'] : 345 [1, 'connectedCallback', 2, 'disconnectedCallback', 3, 'adoptedCallback', 4, 'attributeChangedCallback']); 346 assert_array_equals(constructorCalls, [0, 'prototype', 347 6, 'observedAttributes', 348 7, 'disabledFeatures', 349 8, 'formAssociated']); 350 }, 'customElements.define must get "observedAttributes" property on the constructor prototype when "attributeChangedCallback" is present'); 351 352 test(function () { 353 var constructor = function () {} 354 constructor.prototype.attributeChangedCallback = function () { }; 355 var calls = []; 356 var err = {name: 'expectedError'}; 357 var proxy = new Proxy(constructor, { 358 get: function (target, name) { 359 calls.push(name); 360 if (name == 'observedAttributes') 361 throw err; 362 return target[name]; 363 } 364 }); 365 assert_throws_exactly(err, function () { customElements.define('element-with-throwing-observed-attributes', proxy); }); 366 assert_array_equals(calls, ['prototype', 'observedAttributes'], 367 'customElements.define must get "prototype" and "observedAttributes" on the constructor'); 368 }, 'customElements.define must rethrow an exception thrown while getting observedAttributes on the constructor prototype'); 369 370 test(function () { 371 var constructor = function () {} 372 constructor.prototype.attributeChangedCallback = function () { }; 373 var calls = []; 374 var proxy = new Proxy(constructor, { 375 get: function (target, name) { 376 calls.push(name); 377 if (name == 'observedAttributes') 378 return 1; 379 return target[name]; 380 } 381 }); 382 assert_throws_js(TypeError, function () { customElements.define('element-with-invalid-observed-attributes', proxy); }); 383 assert_array_equals(calls, ['prototype', 'observedAttributes'], 384 'customElements.define must get "prototype" and "observedAttributes" on the constructor'); 385 }, 'customElements.define must rethrow an exception thrown while converting the value of observedAttributes to sequence<DOMString>'); 386 387 test(function () { 388 var err = {name: 'SomeError'}; 389 var constructor = function () {} 390 constructor.prototype.attributeChangedCallback = function () { }; 391 constructor.observedAttributes = {[Symbol.iterator]: function *() { 392 yield 'foo'; 393 throw err; 394 }}; 395 assert_throws_exactly(err, function () { customElements.define('element-with-generator-observed-attributes', constructor); }); 396 }, 'customElements.define must rethrow an exception thrown while iterating over observedAttributes to sequence<DOMString>'); 397 398 test(function () { 399 var constructor = function () {} 400 constructor.prototype.attributeChangedCallback = function () { }; 401 constructor.observedAttributes = {[Symbol.iterator]: 1}; 402 assert_throws_js(TypeError, function () { customElements.define('element-with-observed-attributes-with-uncallable-iterator', constructor); }); 403 }, 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on observedAttributes'); 404 405 test(function () { 406 var constructor = function () {} 407 constructor.observedAttributes = 1; 408 customElements.define('element-without-callback-with-invalid-observed-attributes', constructor); 409 }, 'customElements.define must not throw even if "observedAttributes" fails to convert if "attributeChangedCallback" is not defined'); 410 411 test(function () { 412 var constructor = function () {} 413 var calls = []; 414 var err = {name: 'expectedError'} 415 var proxy = new Proxy(constructor, { 416 get: function (target, name) { 417 calls.push(name); 418 if (name == 'disabledFeatures') 419 throw err; 420 return target[name]; 421 } 422 }); 423 assert_throws_exactly(err, () => customElements.define('element-with-throwing-disabled-features', proxy)); 424 assert_array_equals(calls, ['prototype', 'disabledFeatures'], 425 'customElements.define must get "prototype" and "disabledFeatures" on the constructor'); 426 }, 'customElements.define must rethrow an exception thrown while getting disabledFeatures on the constructor prototype'); 427 428 test(function () { 429 var constructor = function () {} 430 var calls = []; 431 var proxy = new Proxy(constructor, { 432 get: function (target, name) { 433 calls.push(name); 434 if (name == 'disabledFeatures') 435 return 1; 436 return target[name]; 437 } 438 }); 439 assert_throws_js(TypeError, () => customElements.define('element-with-invalid-disabled-features', proxy)); 440 assert_array_equals(calls, ['prototype', 'disabledFeatures'], 441 'customElements.define must get "prototype" and "disabledFeatures" on the constructor'); 442 }, 'customElements.define must rethrow an exception thrown while converting the value of disabledFeatures to sequence<DOMString>'); 443 444 test(function () { 445 var constructor = function () {} 446 var err = {name: 'SomeError'}; 447 constructor.disabledFeatures = {[Symbol.iterator]: function *() { 448 yield 'foo'; 449 throw err; 450 }}; 451 assert_throws_exactly(err, () => customElements.define('element-with-generator-disabled-features', constructor)); 452 }, 'customElements.define must rethrow an exception thrown while iterating over disabledFeatures to sequence<DOMString>'); 453 454 test(function () { 455 var constructor = function () {} 456 constructor.disabledFeatures = {[Symbol.iterator]: 1}; 457 assert_throws_js(TypeError, () => customElements.define('element-with-disabled-features-with-uncallable-iterator', constructor)); 458 }, 'customElements.define must rethrow an exception thrown while retrieving Symbol.iterator on disabledFeatures'); 459 460 test(function () { 461 var constructor = function () {} 462 var calls = []; 463 var err = {name: 'expectedError'}; 464 var proxy = new Proxy(constructor, { 465 get: function (target, name) { 466 calls.push(name); 467 if (name == 'formAssociated') 468 throw err; 469 return target[name]; 470 } 471 }); 472 assert_throws_exactly(err, 473 () => customElements.define('element-with-throwing-form-associated', proxy)); 474 assert_array_equals(calls, ['prototype', 'disabledFeatures', 'formAssociated'], 475 'customElements.define must get "prototype", "disabledFeatures", and ' + 476 '"formAssociated" on the constructor'); 477 }, 'customElements.define must rethrow an exception thrown while getting ' + 478 'formAssociated on the constructor prototype'); 479 480 test(function () { 481 var constructor = function () {} 482 var prototypeCalls = []; 483 constructor.prototype = new Proxy(constructor.prototype, { 484 get: function(target, name) { 485 prototypeCalls.push(name) 486 return target[name]; 487 } 488 }); 489 var constructorCalls = []; 490 var proxy = new Proxy(constructor, { 491 get: function (target, name) { 492 constructorCalls.push(name); 493 if (name == 'formAssociated') 494 return 1; 495 return target[name]; 496 } 497 }); 498 customElements.define('element-with-form-associated-true', proxy); 499 assert_array_equals(constructorCalls, 500 ['prototype', 'disabledFeatures', 'formAssociated'], 501 'customElements.define must get "prototype", "disabledFeatures", and ' + 502 '"formAssociated" on the constructor'); 503 assert_array_equals( 504 prototypeCalls, 505 moveBefore_supported ? ['connectedCallback', 'disconnectedCallback', 'connectedMoveCallback', 'adoptedCallback', 506 'attributeChangedCallback', 'formAssociatedCallback', 507 'formResetCallback', 'formDisabledCallback', 508 'formStateRestoreCallback'] : 509 ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 510 'attributeChangedCallback', 'formAssociatedCallback', 511 'formResetCallback', 'formDisabledCallback', 512 'formStateRestoreCallback'], 513 'customElements.define must get 8 callbacks on the prototype'); 514 }, 'customElements.define must get four additional callbacks on the prototype' + 515 ' if formAssociated is converted to true'); 516 517 test(function () { 518 var constructor = function() {}; 519 var proxy = new Proxy(constructor, { 520 get: function(target, name) { 521 if (name == 'formAssociated') 522 return {}; // Any object is converted to 'true'. 523 return target[name]; 524 } 525 }); 526 var calls = []; 527 var err = {name: 'expectedError'}; 528 constructor.prototype = new Proxy(constructor.prototype, { 529 get: function (target, name) { 530 calls.push(name); 531 if (name == 'formDisabledCallback') 532 throw err; 533 return target[name]; 534 } 535 }); 536 assert_throws_exactly(err, 537 () => customElements.define('element-with-throwing-callback-2', proxy)); 538 assert_array_equals(calls, moveBefore_supported ? ['connectedCallback', 'disconnectedCallback', 'connectedMoveCallback', 539 'adoptedCallback', 'attributeChangedCallback', 540 'formAssociatedCallback', 'formResetCallback', 541 'formDisabledCallback'] : ['connectedCallback', 'disconnectedCallback', 542 'adoptedCallback', 'attributeChangedCallback', 543 'formAssociatedCallback', 'formResetCallback', 544 'formDisabledCallback'], 545 'customElements.define must not get callbacks after one of the get throws'); 546 547 var calls2 = []; 548 constructor.prototype = new Proxy(constructor.prototype, { 549 get: function (target, name) { 550 calls2.push(name); 551 if (name == 'formResetCallback') 552 return 43; // Can't convert to a Function. 553 return target[name]; 554 } 555 }); 556 assert_throws_js(TypeError, 557 () => customElements.define('element-with-throwing-callback-3', proxy)); 558 assert_array_equals(calls2, moveBefore_supported ? ['connectedCallback', 'disconnectedCallback', 'connectedMoveCallback', 559 'adoptedCallback', 'attributeChangedCallback', 560 'formAssociatedCallback', 'formResetCallback'] : ['connectedCallback', 'disconnectedCallback', 561 'adoptedCallback', 'attributeChangedCallback', 562 'formAssociatedCallback', 'formResetCallback'], 563 'customElements.define must not get callbacks after one of the get throws'); 564 }, 'customElements.define must rethrow an exception thrown while getting ' + 565 'additional formAssociated callbacks on the constructor prototype'); 566 567 test(function () { 568 class MyCustomElement extends HTMLElement {}; 569 customElements.define('my-custom-element', MyCustomElement); 570 571 var instance = new MyCustomElement; 572 assert_true(instance instanceof MyCustomElement, 573 'An instance of a custom HTML element be an instance of the associated interface'); 574 575 assert_true(instance instanceof HTMLElement, 576 'An instance of a custom HTML element must inherit from HTMLElement'); 577 578 assert_equals(instance.localName, 'my-custom-element', 579 'An instance of a custom element must use the associated tag name'); 580 581 assert_equals(instance.namespaceURI, 'http://www.w3.org/1999/xhtml', 582 'A custom element HTML must use HTML namespace'); 583 584 }, 'customElements.define must define an instantiatable custom element'); 585 586 test(function () { 587 var disconnectedElement = document.createElement('some-custom'); 588 var connectedElementBeforeShadowHost = document.createElement('some-custom'); 589 var connectedElementAfterShadowHost = document.createElement('some-custom'); 590 var elementInShadowTree = document.createElement('some-custom'); 591 var childElementOfShadowHost = document.createElement('some-custom'); 592 var customShadowHost = document.createElement('some-custom'); 593 var elementInNestedShadowTree = document.createElement('some-custom'); 594 595 var container = document.createElement('div'); 596 var shadowHost = document.createElement('div'); 597 var shadowRoot = shadowHost.attachShadow({mode: 'closed'}); 598 container.appendChild(connectedElementBeforeShadowHost); 599 container.appendChild(shadowHost); 600 container.appendChild(connectedElementAfterShadowHost); 601 shadowHost.appendChild(childElementOfShadowHost); 602 shadowRoot.appendChild(elementInShadowTree); 603 shadowRoot.appendChild(customShadowHost); 604 605 var innerShadowRoot = customShadowHost.attachShadow({mode: 'closed'}); 606 innerShadowRoot.appendChild(elementInNestedShadowTree); 607 608 var calls = []; 609 class SomeCustomElement extends HTMLElement { 610 constructor() { 611 super(); 612 calls.push(this); 613 } 614 }; 615 616 document.body.appendChild(container); 617 customElements.define('some-custom', SomeCustomElement); 618 assert_array_equals(calls, [connectedElementBeforeShadowHost, elementInShadowTree, customShadowHost, elementInNestedShadowTree, childElementOfShadowHost, connectedElementAfterShadowHost]); 619 }, 'customElements.define must upgrade elements in the shadow-including tree order'); 620 621 test(function () { 622 assert_true('get' in CustomElementRegistry.prototype, '"get" exists on CustomElementRegistry.prototype'); 623 assert_true('get' in customElements, '"get" exists on window.customElements'); 624 }, 'CustomElementRegistry interface must have get as a method'); 625 626 test(function () { 627 assert_equals(customElements.get('a-b'), undefined); 628 }, 'customElements.get must return undefined when the registry does not contain an entry with the given name'); 629 630 test(function () { 631 assert_equals(customElements.get('html'), undefined); 632 assert_equals(customElements.get('span'), undefined); 633 assert_equals(customElements.get('div'), undefined); 634 assert_equals(customElements.get('g'), undefined); 635 assert_equals(customElements.get('ab'), undefined); 636 }, 'customElements.get must return undefined when the registry does not contain an entry with the given name even if the name was not a valid custom element name'); 637 638 test(function () { 639 assert_equals(customElements.get('existing-custom-element'), undefined); 640 class ExistingCustomElement extends HTMLElement {}; 641 customElements.define('existing-custom-element', ExistingCustomElement); 642 assert_equals(customElements.get('existing-custom-element'), ExistingCustomElement); 643 }, 'customElements.get return the constructor of the entry with the given name when there is a matching entry.'); 644 645 test(function () { 646 assert_true(customElements.whenDefined('some-name') instanceof Promise); 647 }, 'customElements.whenDefined must return a promise for a valid custom element name'); 648 649 test(function () { 650 assert_equals(customElements.whenDefined('some-name'), customElements.whenDefined('some-name')); 651 }, 'customElements.whenDefined must return the same promise each time invoked for a valid custom element name which has not been defined'); 652 653 promise_test(function () { 654 var resolved = false; 655 var rejected = false; 656 customElements.whenDefined('a-b').then(function () { resolved = true; }, function () { rejected = true; }); 657 return Promise.resolve().then(function () { 658 assert_false(resolved, 'The promise returned by "whenDefined" must not be resolved until a custom element is defined'); 659 assert_false(rejected, 'The promise returned by "whenDefined" must not be rejected until a custom element is defined'); 660 }); 661 }, 'customElements.whenDefined must return an unresolved promise when the registry does not contain the entry with the given name') 662 663 promise_test(function () { 664 var promise = customElements.whenDefined('badname'); 665 promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; }); 666 667 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 668 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 669 670 return Promise.resolve().then(function () { 671 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 672 assert_true('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 673 }); 674 }, 'customElements.whenDefined must return a rejected promise when the given name is not a valid custom element name'); 675 676 promise_test(function () { 677 class PreexistingCustomElement extends HTMLElement { }; 678 customElements.define('preexisting-custom-element', PreexistingCustomElement); 679 680 var promise = customElements.whenDefined('preexisting-custom-element'); 681 promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; }); 682 683 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 684 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 685 686 return Promise.resolve().then(function () { 687 assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 688 assert_equals(promise.resolved, PreexistingCustomElement, 689 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined'); 690 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 691 }); 692 }, 'customElements.whenDefined must return a resolved promise when the registry contains the entry with the given name'); 693 694 promise_test(function () { 695 class AnotherExistingCustomElement extends HTMLElement {}; 696 customElements.define('another-existing-custom-element', AnotherExistingCustomElement); 697 698 var promise1 = customElements.whenDefined('another-existing-custom-element'); 699 var promise2 = customElements.whenDefined('another-existing-custom-element'); 700 promise1.then(function (value) { promise1.resolved = value; }, function (value) { promise1.rejected = value; }); 701 promise2.then(function (value) { promise2.resolved = value; }, function (value) { promise2.rejected = value; }); 702 703 assert_not_equals(promise1, promise2); 704 assert_false('resolved' in promise1, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 705 assert_false('resolved' in promise2, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 706 assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 707 assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 708 709 return Promise.resolve().then(function () { 710 assert_true('resolved' in promise1, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 711 assert_equals(promise1.resolved, AnotherExistingCustomElement, 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined'); 712 assert_false('rejected' in promise1, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 713 714 assert_true('resolved' in promise2, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 715 assert_equals(promise2.resolved, AnotherExistingCustomElement, 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined'); 716 assert_false('rejected' in promise2, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 717 }); 718 }, 'customElements.whenDefined must return a new resolved promise each time invoked when the registry contains the entry with the given name'); 719 720 promise_test(function () { 721 class ElementDefinedAfterWhenDefined extends HTMLElement { }; 722 var promise = customElements.whenDefined('element-defined-after-whendefined'); 723 promise.then(function (value) { promise.resolved = value; }, function (value) { promise.rejected = value; }); 724 725 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 726 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 727 728 var promiseAfterDefine; 729 return Promise.resolve().then(function () { 730 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the element is defined'); 731 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the element is defined'); 732 assert_equals(customElements.whenDefined('element-defined-after-whendefined'), promise, 733 '"whenDefined" must return the same unresolved promise before the custom element is defined'); 734 customElements.define('element-defined-after-whendefined', ElementDefinedAfterWhenDefined); 735 assert_false('resolved' in promise, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 736 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 737 738 promiseAfterDefine = customElements.whenDefined('element-defined-after-whendefined'); 739 promiseAfterDefine.then(function (value) { promiseAfterDefine.resolved = value; }, function (value) { promiseAfterDefine.rejected = value; }); 740 assert_not_equals(promiseAfterDefine, promise, '"whenDefined" must return a resolved promise once the custom element is defined'); 741 assert_false('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be resolved until the end of the next microtask'); 742 assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected until the end of the next microtask'); 743 }).then(function () { 744 assert_true('resolved' in promise, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 745 assert_equals(promise.resolved, ElementDefinedAfterWhenDefined, 746 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined'); 747 assert_false('rejected' in promise, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 748 749 assert_true('resolved' in promiseAfterDefine, 'The promise returned by "whenDefined" must be resolved when a custom element is defined'); 750 assert_equals(promiseAfterDefine.resolved, ElementDefinedAfterWhenDefined, 751 'The promise returned by "whenDefined" must be resolved with the constructor of the element when a custom element is defined'); 752 assert_false('rejected' in promiseAfterDefine, 'The promise returned by "whenDefined" must not be rejected when a custom element is defined'); 753 }); 754 }, 'A promise returned by customElements.whenDefined must be resolved by "define"'); 755 756 promise_test(function () { 757 class ResolvedCustomElement extends HTMLElement {}; 758 customElements.define('resolved-custom-element', ResolvedCustomElement); 759 return customElements.whenDefined('resolved-custom-element').then(function (value) { 760 assert_equals(value, ResolvedCustomElement, 'The promise returned by "whenDefined" must resolve with the defined class'); 761 }); 762 }, 'A promise returned by customElements.whenDefined must be resolved with the defined class'); 763 764 promise_test(function () { 765 var promise = customElements.whenDefined('not-resolved-yet-custom-element').then(function (value) { 766 assert_equals(value, NotResolvedYetCustomElement, 'The promise returned by "whenDefined" must resolve with the defined class once such class is defined'); 767 }); 768 class NotResolvedYetCustomElement extends HTMLElement {}; 769 customElements.define('not-resolved-yet-custom-element', NotResolvedYetCustomElement); 770 return promise; 771 }, 'A promise returned by customElements.whenDefined must be resolved with the defined class once such class is defined'); 772 </script> 773 </body> 774 </html>