<!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, "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) {
    var nextTestFunction = testFunctions.shift();
    info(`Start ${nextTestFunction.name} ...`);
    nextTestFunction();
  }
}

SimpleTest.waitForExplicitFinish();

runNextTest();

</script>
</body>
</html>
