<!DOCTYPE HTML> <html> <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=783129 --> <head> <title>Test for custom elements lifecycle callback</title> <script src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> </head> <body> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783129">Bug 783129</a> <div id="container"> <x-hello id="hello"></x-hello> <button id="extbutton" is="x-button"></button> </div> <script> var container = document.getElementById("container"); // Tests callbacks after defining element type that is already in the document. // create element in document -> define -> remove from document function testRegisterUnresolved() { var helloElem = document.getElementById("hello"); var connectedCallbackCalled = false; var disconnectedCallbackCalled = false; class Hello extends HTMLElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); is(this, helloElem, "The 'this' value should be the custom element."); connectedCallbackCalled = true; } disconnectedCallback() { is(connectedCallbackCalled, true, "Connected callback should be called before detached"); is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); disconnectedCallbackCalled = true; is(this, helloElem, "The 'this' value should be the custom element."); runNextTest(); } attributeChangedCallback(name, oldValue, newValue) { ok(false, "attributeChanged callback should never be called in this test."); } }; customElements.define("x-hello", Hello); // Remove element from document to trigger disconnected callback. container.removeChild(helloElem); } // Tests callbacks after defining an extended element type that is already in the document. // create element in document -> define -> remove from document function testRegisterUnresolvedExtended() { var buttonElem = document.getElementById("extbutton"); var connectedCallbackCalled = false; var disconnectedCallbackCalled = false; class XButton extends HTMLButtonElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); is(this, buttonElem, "The 'this' value should be the custom element."); connectedCallbackCalled = true; } disconnectedCallback() { is(connectedCallbackCalled, true, "Connected callback should be called before detached"); is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); disconnectedCallbackCalled = true; is(this, buttonElem, "The 'this' value should be the custom element."); runNextTest(); } attributeChangedCallback(name, oldValue, newValue) { ok(false, "attributeChanged callback should never be called in this test."); } }; customElements.define("x-button", XButton, { extends: "button" }); // Remove element from document to trigger disconnected callback. container.removeChild(buttonElem); } function testInnerHTML() { var connectedCallbackCalled = false; class XInnerHTML extends HTMLElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); connectedCallbackCalled = true; } }; customElements.define("x-inner-html", XInnerHTML); var div = document.createElement(div); document.documentElement.appendChild(div); div.innerHTML = '<x-inner-html></x-inner-html>'; is(connectedCallbackCalled, true, "Connected callback should be called after setting innerHTML."); runNextTest(); } function testInnerHTMLExtended() { var connectedCallbackCalled = false; class XInnerHTMLExtend extends HTMLButtonElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); connectedCallbackCalled = true; } }; customElements.define("x-inner-html-extended", XInnerHTMLExtend, { extends: "button" }); var div = document.createElement(div); document.documentElement.appendChild(div); div.innerHTML = '<button is="x-inner-html-extended"></button>'; is(connectedCallbackCalled, true, "Connected callback should be called after setting innerHTML."); runNextTest(); } function testInnerHTMLUpgrade() { var connectedCallbackCalled = false; var div = document.createElement(div); document.documentElement.appendChild(div); div.innerHTML = '<x-inner-html-upgrade></x-inner-html-upgrade>'; class XInnerHTMLUpgrade extends HTMLElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); connectedCallbackCalled = true; } }; customElements.define("x-inner-html-upgrade", XInnerHTMLUpgrade); is(connectedCallbackCalled, true, "Connected callback should be called after registering."); runNextTest(); } function testInnerHTMLExtendedUpgrade() { var connectedCallbackCalled = false; var div = document.createElement(div); document.documentElement.appendChild(div); div.innerHTML = '<button is="x-inner-html-extended-upgrade"></button>'; class XInnerHTMLExtnedUpgrade extends HTMLButtonElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called once in this test."); connectedCallbackCalled = true; } }; customElements.define("x-inner-html-extended-upgrade", XInnerHTMLExtnedUpgrade, { extends: "button" }); is(connectedCallbackCalled, true, "Connected callback should be called after registering."); runNextTest(); } // Test callback when creating element after defining an element type. // define -> create element -> insert into document -> remove from document function testRegisterResolved() { var connectedCallbackCalled = false; var disconnectedCallbackCalled = false; class Resolved extends HTMLElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called on in this test."); is(this, createdElement, "The 'this' value should be the custom element."); connectedCallbackCalled = true; } disconnectedCallback() { is(connectedCallbackCalled, true, "Connected callback should be called before detached"); is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); is(this, createdElement, "The 'this' value should be the custom element."); disconnectedCallbackCalled = true; runNextTest(); } attributeChangedCallback() { ok(false, "attributeChanged callback should never be called in this test."); } }; customElements.define("x-resolved", Resolved); var createdElement = document.createElement("x-resolved"); is(createdElement.__proto__, Resolved.prototype, "Prototype of custom element should be the defined prototype."); // Insert element into document to trigger attached callback. container.appendChild(createdElement); // Remove element from document to trigger detached callback. container.removeChild(createdElement); } // Callbacks should always be the same ones when registered. function testChangingCallback() { var callbackCalled = false; class TestCallback extends HTMLElement { attributeChangedCallback(aName, aOldValue, aNewValue) { is(callbackCalled, false, "Callback should only be called once in this test."); callbackCalled = true; runNextTest(); } static get observedAttributes() { return ["data-foo"]; } } customElements.define("x-test-callback", TestCallback); TestCallback.prototype.attributeChangedCallback = function(name, oldValue, newValue) { ok(false, "Only callbacks at registration should be called."); }; var elem = document.createElement("x-test-callback"); elem.setAttribute("data-foo", "bar"); } function testAttributeChanged() { var createdElement; // Sequence of callback arguments that we expect from attribute changed callback // after changing attributes values in a specific order. var expectedCallbackArguments = [ // [oldValue, newValue] [null, "newvalue"], // Setting the attribute value to "newvalue" ["newvalue", "nextvalue"], // Changing the attribute value from "newvalue" to "nextvalue" ["nextvalue", ""], // Changing the attribute value from "nextvalue" to empty string ["", null], // Removing the attribute. ]; class AttrChange extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { is(this, createdElement, "The 'this' value should be the custom element."); ok(expectedCallbackArguments.length > 0, "Attribute changed callback should not be called more than expected."); is(name, "changeme", "name arugment in attribute changed callback should be the name of the changed attribute."); var expectedArgs = expectedCallbackArguments.shift(); is(oldValue, expectedArgs[0], "The old value argument should match the expected value."); is(newValue, expectedArgs[1], "The new value argument should match the expected value."); if (expectedCallbackArguments.length === 0) { // Done with the attribute changed callback test. runNextTest(); } } static get observedAttributes() { return ["changeme"]; } } customElements.define("x-attrchange", AttrChange); createdElement = document.createElement("x-attrchange"); createdElement.setAttribute("changeme", "newvalue"); createdElement.setAttribute("changeme", "nextvalue"); createdElement.setAttribute("changeme", ""); createdElement.removeAttribute("changeme"); } function testAttributeChangedExtended() { var callbackCalled = false; class ExtnededAttributeChange extends HTMLButtonElement { attributeChangedCallback(name, oldValue, newValue) { is(callbackCalled, false, "Callback should only be called once in this test."); callbackCalled = true; runNextTest(); } static get observedAttributes() { return ["foo"]; } } customElements.define("x-extended-attribute-change", ExtnededAttributeChange, { extends: "button" }); var elem = document.createElement("button", {is: "x-extended-attribute-change"}); elem.setAttribute("foo", "bar"); } function testStyleAttributeChange() { var expectedCallbackArguments = [ // [name, oldValue, newValue] ["style", null, "font-size: 10px;"], ["style", "font-size: 10px;", "font-size: 20px;"], ["style", "font-size: 20px;", "font-size: 30px;"], ]; customElements.define("x-style-attribute-change", class extends HTMLElement { attributeChangedCallback(name, oldValue, newValue) { if (expectedCallbackArguments.length === 0) { ok(false, "Got unexpected attributeChangedCallback?"); return; } let expectedArgument = expectedCallbackArguments.shift(); is(name, expectedArgument[0], "The name argument should match the expected value."); is(oldValue, expectedArgument[1], "The old value argument should match the expected value."); is(newValue, expectedArgument[2], "The new value argument should match the expected value."); } static get observedAttributes() { return ["style"]; } }); var elem = document.createElement("x-style-attribute-change"); elem.style.fontSize = "10px"; elem.style.fontSize = "20px"; elem.style.fontSize = "30px"; ok(expectedCallbackArguments.length === 0, "The attributeChangedCallback should be fired synchronously."); runNextTest(); } // Creates a custom element that is an upgrade candidate (no registration) // and mutate the element in ways that would call callbacks for registered // elements. function testUpgradeCandidate() { var createdElement = document.createElement("x-upgrade-candidate"); container.appendChild(createdElement); createdElement.setAttribute("foo", "bar"); container.removeChild(createdElement); ok(true, "Nothing bad should happen when trying to mutating upgrade candidates."); runNextTest(); } function testNotInDocEnterLeave() { class DestinedForFragment extends HTMLElement { connectedCallback() { ok(false, "Connected callback should not be called."); } disconnectedCallback() { ok(false, "Disconnected callback should not be called."); } }; var createdElement = document.createElement("x-destined-for-fragment"); customElements.define("x-destined-for-fragment", DestinedForFragment); var fragment = new DocumentFragment(); fragment.appendChild(createdElement); fragment.removeChild(createdElement); var divNotInDoc = document.createElement("div"); divNotInDoc.appendChild(createdElement); divNotInDoc.removeChild(createdElement); runNextTest(); } function testEnterLeaveView() { var connectedCallbackCalled = false; var disconnectedCallbackCalled = false; class ElementInDiv extends HTMLElement { connectedCallback() { is(connectedCallbackCalled, false, "Connected callback should only be called on in this test."); connectedCallbackCalled = true; } disconnectedCallback() { is(connectedCallbackCalled, true, "Connected callback should be called before detached"); is(disconnectedCallbackCalled, false, "Disconnected callback should only be called once in this test."); disconnectedCallbackCalled = true; runNextTest(); } }; var div = document.createElement("div"); customElements.define("x-element-in-div", ElementInDiv); var customElement = document.createElement("x-element-in-div"); div.appendChild(customElement); is(connectedCallbackCalled, false, "Appending a custom element to a node that is not in the document should not call the connected callback."); container.appendChild(div); container.removeChild(div); } var testFunctions = [ testRegisterUnresolved, testRegisterUnresolvedExtended, testInnerHTML, testInnerHTMLExtended, testInnerHTMLUpgrade, testInnerHTMLExtendedUpgrade, testRegisterResolved, testAttributeChanged, testAttributeChangedExtended, testStyleAttributeChange, testUpgradeCandidate, testChangingCallback, testNotInDocEnterLeave, testEnterLeaveView, SimpleTest.finish ]; function runNextTest() { if (testFunctions.length > 0) { var nextTestFunction = testFunctions.shift(); info(`Start ${nextTestFunction.name} ...`); nextTestFunction(); } } SimpleTest.waitForExplicitFinish(); runNextTest(); </script> </body> </html>