test_custom_element_lifecycle.html (14786B)
1 <!DOCTYPE HTML> 2 <html> 3 <!-- 4 https://bugzilla.mozilla.org/show_bug.cgi?id=783129 5 --> 6 <head> 7 <title>Test for custom elements lifecycle callback</title> 8 <script src="/tests/SimpleTest/SimpleTest.js"></script> 9 <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> 10 </head> 11 <body> 12 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783129">Bug 783129</a> 13 <div id="container"> 14 <x-hello id="hello"></x-hello> 15 <button id="extbutton" is="x-button"></button> 16 </div> 17 <script> 18 19 var container = document.getElementById("container"); 20 21 // Tests callbacks after defining element type that is already in the document. 22 // create element in document -> define -> remove from document 23 function testRegisterUnresolved() { 24 var helloElem = document.getElementById("hello"); 25 26 var connectedCallbackCalled = false; 27 var disconnectedCallbackCalled = false; 28 29 class Hello extends HTMLElement { 30 connectedCallback() { 31 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 32 is(this, helloElem, "The 'this' value should be the custom element."); 33 connectedCallbackCalled = true; 34 } 35 36 disconnectedCallback() { 37 is(connectedCallbackCalled, true, "Connected callback should be called before detached"); 38 is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); 39 disconnectedCallbackCalled = true; 40 is(this, helloElem, "The 'this' value should be the custom element."); 41 runNextTest(); 42 } 43 44 attributeChangedCallback(name, oldValue, newValue) { 45 ok(false, "attributeChanged callback should never be called in this test."); 46 } 47 }; 48 49 customElements.define("x-hello", Hello); 50 51 // Remove element from document to trigger disconnected callback. 52 container.removeChild(helloElem); 53 } 54 55 // Tests callbacks after defining an extended element type that is already in the document. 56 // create element in document -> define -> remove from document 57 function testRegisterUnresolvedExtended() { 58 var buttonElem = document.getElementById("extbutton"); 59 60 var connectedCallbackCalled = false; 61 var disconnectedCallbackCalled = false; 62 63 class XButton extends HTMLButtonElement { 64 connectedCallback() { 65 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 66 is(this, buttonElem, "The 'this' value should be the custom element."); 67 connectedCallbackCalled = true; 68 } 69 70 disconnectedCallback() { 71 is(connectedCallbackCalled, true, "Connected callback should be called before detached"); 72 is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); 73 disconnectedCallbackCalled = true; 74 is(this, buttonElem, "The 'this' value should be the custom element."); 75 runNextTest(); 76 } 77 78 attributeChangedCallback(name, oldValue, newValue) { 79 ok(false, "attributeChanged callback should never be called in this test."); 80 } 81 }; 82 83 customElements.define("x-button", XButton, { extends: "button" }); 84 85 // Remove element from document to trigger disconnected callback. 86 container.removeChild(buttonElem); 87 } 88 89 function testInnerHTML() { 90 var connectedCallbackCalled = false; 91 92 class XInnerHTML extends HTMLElement { 93 connectedCallback() { 94 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 95 connectedCallbackCalled = true; 96 } 97 }; 98 99 customElements.define("x-inner-html", XInnerHTML); 100 var div = document.createElement(div); 101 document.documentElement.appendChild(div); 102 div.innerHTML = '<x-inner-html></x-inner-html>'; 103 is(connectedCallbackCalled, true, "Connected callback should be called after setting innerHTML."); 104 runNextTest(); 105 } 106 107 function testInnerHTMLExtended() { 108 var connectedCallbackCalled = false; 109 110 class XInnerHTMLExtend extends HTMLButtonElement { 111 connectedCallback() { 112 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 113 connectedCallbackCalled = true; 114 } 115 }; 116 117 customElements.define("x-inner-html-extended", XInnerHTMLExtend, { extends: "button" }); 118 var div = document.createElement(div); 119 document.documentElement.appendChild(div); 120 div.innerHTML = '<button is="x-inner-html-extended"></button>'; 121 is(connectedCallbackCalled, true, "Connected callback should be called after setting innerHTML."); 122 runNextTest(); 123 } 124 125 function testInnerHTMLUpgrade() { 126 var connectedCallbackCalled = false; 127 128 var div = document.createElement(div); 129 document.documentElement.appendChild(div); 130 div.innerHTML = '<x-inner-html-upgrade></x-inner-html-upgrade>'; 131 132 class XInnerHTMLUpgrade extends HTMLElement { 133 connectedCallback() { 134 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 135 connectedCallbackCalled = true; 136 } 137 }; 138 139 customElements.define("x-inner-html-upgrade", XInnerHTMLUpgrade); 140 is(connectedCallbackCalled, true, "Connected callback should be called after registering."); 141 runNextTest(); 142 } 143 144 function testInnerHTMLExtendedUpgrade() { 145 var connectedCallbackCalled = false; 146 147 var div = document.createElement(div); 148 document.documentElement.appendChild(div); 149 div.innerHTML = '<button is="x-inner-html-extended-upgrade"></button>'; 150 151 class XInnerHTMLExtnedUpgrade extends HTMLButtonElement { 152 connectedCallback() { 153 is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); 154 connectedCallbackCalled = true; 155 } 156 }; 157 158 customElements.define("x-inner-html-extended-upgrade", XInnerHTMLExtnedUpgrade, { extends: "button" }); 159 is(connectedCallbackCalled, true, "Connected callback should be called after registering."); 160 runNextTest(); 161 } 162 163 // Test callback when creating element after defining an element type. 164 // define -> create element -> insert into document -> remove from document 165 function testRegisterResolved() { 166 var connectedCallbackCalled = false; 167 var disconnectedCallbackCalled = false; 168 169 class Resolved extends HTMLElement { 170 connectedCallback() { 171 is(connectedCallbackCalled, false, "Connected callback should only be called on in this test."); 172 is(this, createdElement, "The 'this' value should be the custom element."); 173 connectedCallbackCalled = true; 174 } 175 176 disconnectedCallback() { 177 is(connectedCallbackCalled, true, "Connected callback should be called before detached"); 178 is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); 179 is(this, createdElement, "The 'this' value should be the custom element."); 180 disconnectedCallbackCalled = true; 181 runNextTest(); 182 } 183 184 attributeChangedCallback() { 185 ok(false, "attributeChanged callback should never be called in this test."); 186 } 187 }; 188 189 customElements.define("x-resolved", Resolved); 190 191 var createdElement = document.createElement("x-resolved"); 192 is(createdElement.__proto__, Resolved.prototype, "Prototype of custom element should be the defined prototype."); 193 194 // Insert element into document to trigger attached callback. 195 container.appendChild(createdElement); 196 197 // Remove element from document to trigger detached callback. 198 container.removeChild(createdElement); 199 } 200 201 // Callbacks should always be the same ones when registered. 202 function testChangingCallback() { 203 var callbackCalled = false; 204 205 class TestCallback extends HTMLElement 206 { 207 attributeChangedCallback(aName, aOldValue, aNewValue) { 208 is(callbackCalled, false, "Callback should only be called once in this test."); 209 callbackCalled = true; 210 runNextTest(); 211 } 212 213 static get observedAttributes() { 214 return ["data-foo"]; 215 } 216 } 217 218 customElements.define("x-test-callback", TestCallback); 219 220 TestCallback.prototype.attributeChangedCallback = function(name, oldValue, newValue) { 221 ok(false, "Only callbacks at registration should be called."); 222 }; 223 224 var elem = document.createElement("x-test-callback"); 225 elem.setAttribute("data-foo", "bar"); 226 } 227 228 function testAttributeChanged() { 229 var createdElement; 230 // Sequence of callback arguments that we expect from attribute changed callback 231 // after changing attributes values in a specific order. 232 var expectedCallbackArguments = [ 233 // [oldValue, newValue] 234 [null, "newvalue"], // Setting the attribute value to "newvalue" 235 ["newvalue", "nextvalue"], // Changing the attribute value from "newvalue" to "nextvalue" 236 ["nextvalue", ""], // Changing the attribute value from "nextvalue" to empty string 237 ["", null], // Removing the attribute. 238 ]; 239 240 class AttrChange extends HTMLElement 241 { 242 attributeChangedCallback(name, oldValue, newValue) { 243 is(this, createdElement, "The 'this' value should be the custom element."); 244 ok(expectedCallbackArguments.length, "Attribute changed callback should not be called more than expected."); 245 246 is(name, "changeme", "name arugment in attribute changed callback should be the name of the changed attribute."); 247 248 var expectedArgs = expectedCallbackArguments.shift(); 249 is(oldValue, expectedArgs[0], "The old value argument should match the expected value."); 250 is(newValue, expectedArgs[1], "The new value argument should match the expected value."); 251 252 if (expectedCallbackArguments.length === 0) { 253 // Done with the attribute changed callback test. 254 runNextTest(); 255 } 256 } 257 258 static get observedAttributes() { 259 return ["changeme"]; 260 } 261 } 262 263 customElements.define("x-attrchange", AttrChange); 264 265 createdElement = document.createElement("x-attrchange"); 266 createdElement.setAttribute("changeme", "newvalue"); 267 createdElement.setAttribute("changeme", "nextvalue"); 268 createdElement.setAttribute("changeme", ""); 269 createdElement.removeAttribute("changeme"); 270 } 271 272 function testAttributeChangedExtended() { 273 var callbackCalled = false; 274 275 class ExtnededAttributeChange extends HTMLButtonElement 276 { 277 attributeChangedCallback(name, oldValue, newValue) { 278 is(callbackCalled, false, "Callback should only be called once in this test."); 279 callbackCalled = true; 280 runNextTest(); 281 } 282 283 static get observedAttributes() { 284 return ["foo"]; 285 } 286 } 287 288 customElements.define("x-extended-attribute-change", ExtnededAttributeChange, 289 { extends: "button" }); 290 291 var elem = document.createElement("button", {is: "x-extended-attribute-change"}); 292 elem.setAttribute("foo", "bar"); 293 } 294 295 function testStyleAttributeChange() { 296 var expectedCallbackArguments = [ 297 // [name, oldValue, newValue] 298 ["style", null, "font-size: 10px;"], 299 ["style", "font-size: 10px;", "font-size: 20px;"], 300 ["style", "font-size: 20px;", "font-size: 30px;"], 301 ]; 302 303 customElements.define("x-style-attribute-change", class extends HTMLElement { 304 attributeChangedCallback(name, oldValue, newValue) { 305 if (expectedCallbackArguments.length === 0) { 306 ok(false, "Got unexpected attributeChangedCallback?"); 307 return; 308 } 309 310 let expectedArgument = expectedCallbackArguments.shift(); 311 is(name, expectedArgument[0], 312 "The name argument should match the expected value."); 313 is(oldValue, expectedArgument[1], 314 "The old value argument should match the expected value."); 315 is(newValue, expectedArgument[2], 316 "The new value argument should match the expected value."); 317 } 318 319 static get observedAttributes() { 320 return ["style"]; 321 } 322 }); 323 324 var elem = document.createElement("x-style-attribute-change"); 325 elem.style.fontSize = "10px"; 326 elem.style.fontSize = "20px"; 327 elem.style.fontSize = "30px"; 328 329 ok(expectedCallbackArguments.length === 0, 330 "The attributeChangedCallback should be fired synchronously."); 331 runNextTest(); 332 } 333 334 // Creates a custom element that is an upgrade candidate (no registration) 335 // and mutate the element in ways that would call callbacks for registered 336 // elements. 337 function testUpgradeCandidate() { 338 var createdElement = document.createElement("x-upgrade-candidate"); 339 container.appendChild(createdElement); 340 createdElement.setAttribute("foo", "bar"); 341 container.removeChild(createdElement); 342 ok(true, "Nothing bad should happen when trying to mutating upgrade candidates."); 343 runNextTest(); 344 } 345 346 function testNotInDocEnterLeave() { 347 class DestinedForFragment extends HTMLElement { 348 connectedCallback() { 349 ok(false, "Connected callback should not be called."); 350 } 351 352 disconnectedCallback() { 353 ok(false, "Disconnected callback should not be called."); 354 } 355 }; 356 357 var createdElement = document.createElement("x-destined-for-fragment"); 358 359 customElements.define("x-destined-for-fragment", DestinedForFragment); 360 361 var fragment = new DocumentFragment(); 362 fragment.appendChild(createdElement); 363 fragment.removeChild(createdElement); 364 365 var divNotInDoc = document.createElement("div"); 366 divNotInDoc.appendChild(createdElement); 367 divNotInDoc.removeChild(createdElement); 368 369 runNextTest(); 370 } 371 372 function testEnterLeaveView() { 373 var connectedCallbackCalled = false; 374 var disconnectedCallbackCalled = false; 375 376 class ElementInDiv extends HTMLElement { 377 connectedCallback() { 378 is(connectedCallbackCalled, false, "Connected callback should only be called on in this test."); 379 connectedCallbackCalled = true; 380 } 381 382 disconnectedCallback() { 383 is(connectedCallbackCalled, true, "Connected callback should be called before detached"); 384 is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); 385 disconnectedCallbackCalled = true; 386 runNextTest(); 387 } 388 }; 389 390 var div = document.createElement("div"); 391 customElements.define("x-element-in-div", ElementInDiv); 392 var customElement = document.createElement("x-element-in-div"); 393 div.appendChild(customElement); 394 is(connectedCallbackCalled, false, "Appending a custom element to a node that is not in the document should not call the connected callback."); 395 396 container.appendChild(div); 397 container.removeChild(div); 398 } 399 400 var testFunctions = [ 401 testRegisterUnresolved, 402 testRegisterUnresolvedExtended, 403 testInnerHTML, 404 testInnerHTMLExtended, 405 testInnerHTMLUpgrade, 406 testInnerHTMLExtendedUpgrade, 407 testRegisterResolved, 408 testAttributeChanged, 409 testAttributeChangedExtended, 410 testStyleAttributeChange, 411 testUpgradeCandidate, 412 testChangingCallback, 413 testNotInDocEnterLeave, 414 testEnterLeaveView, 415 SimpleTest.finish 416 ]; 417 418 function runNextTest() { 419 if (testFunctions.length) { 420 var nextTestFunction = testFunctions.shift(); 421 info(`Start ${nextTestFunction.name} ...`); 422 nextTestFunction(); 423 } 424 } 425 426 SimpleTest.waitForExplicitFinish(); 427 428 runNextTest(); 429 430 </script> 431 </body> 432 </html>