<!DOCTYPE html>
<html>
<head>
<title>Custom Elements: Upgrading</title>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<meta name="assert" content="Node.prototype.cloneNode should upgrade a custom element">
<link rel="help" href="https://html.spec.whatwg.org/#upgrades">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../resources/custom-elements-helpers.js"></script>
</head>
<body>
<div id="log"></div>
<script>
setup({allow_uncaught_exception:true});

test(function () {
    class MyCustomElement extends HTMLElement {}
    customElements.define('my-custom-element', MyCustomElement);

    var instance = document.createElement('my-custom-element');
    assert_true(instance instanceof HTMLElement);
    assert_true(instance instanceof MyCustomElement);

    var clone = instance.cloneNode(false);
    assert_not_equals(instance, clone);
    assert_true(clone instanceof HTMLElement,
        'A cloned custom element must be an instance of HTMLElement');
    assert_true(clone instanceof MyCustomElement,
        'A cloned custom element must be an instance of the custom element');
}, 'Node.prototype.cloneNode(false) must be able to clone a custom element');

test(function () {
    class AutonomousCustomElement extends HTMLElement {};
    class IsCustomElement extends HTMLElement {};

    customElements.define('autonomous-custom-element', AutonomousCustomElement);
    customElements.define('is-custom-element', IsCustomElement);

    var instance = document.createElement('autonomous-custom-element', { is: "is-custom-element"});
    assert_true(instance instanceof HTMLElement);
    assert_true(instance instanceof AutonomousCustomElement);

    var clone = instance.cloneNode(false);
    assert_not_equals(instance, clone);
    assert_true(clone instanceof HTMLElement,
        'A cloned custom element must be an instance of HTMLElement');
    assert_true(clone instanceof AutonomousCustomElement,
        'A cloned custom element must be an instance of the custom element');
}, 'Node.prototype.cloneNode(false) must be able to clone as a autonomous custom element when it contains is attribute');

test_with_window(function (contentWindow) {
    var contentDocument = contentWindow.document;
    class MyCustomElement extends contentWindow.HTMLElement {}
    contentWindow.customElements.define('my-custom-element', MyCustomElement);

    var instance = contentDocument.createElement('my-custom-element');
    assert_true(instance instanceof contentWindow.HTMLElement);
    assert_true(instance instanceof MyCustomElement);

    var clone = instance.cloneNode(false);
    assert_not_equals(instance, clone);
    assert_true(clone instanceof contentWindow.HTMLElement,
        'A cloned custom element must be an instance of HTMLElement');
    assert_true(clone instanceof MyCustomElement,
        'A cloned custom element must be an instance of the custom element');
}, 'Node.prototype.cloneNode(false) must be able to clone a custom element inside an iframe');

test_with_window(function (contentWindow) {
    var contentDocument = contentWindow.document;
    class MyCustomElement extends contentWindow.HTMLElement { }
    contentWindow.customElements.define('my-custom-element', MyCustomElement);

    var instance = contentDocument.createElement('my-custom-element');
    var container = contentDocument.createElement('div');
    container.appendChild(instance);

    var containerClone = container.cloneNode(true);
    assert_true(containerClone instanceof contentWindow.HTMLDivElement);

    var clone = containerClone.firstChild;
    assert_not_equals(instance, clone);
    assert_true(clone instanceof contentWindow.HTMLElement,
        'A cloned custom element must be an instance of HTMLElement');
    assert_true(clone instanceof MyCustomElement,
        'A cloned custom element must be an instance of the custom element');
}, 'Node.prototype.cloneNode(true) must be able to clone a descendent custom element');

test_with_window(function (contentWindow) {
    var parentNodeInConstructor;
    var previousSiblingInConstructor;
    var nextSiblingInConstructor;
    class MyCustomElement extends contentWindow.HTMLElement {
        constructor() {
            super();
            parentNodeInConstructor = this.parentNode;
            previousSiblingInConstructor = this.previousSibling;
            nextSiblingInConstructor = this.nextSibling;
        }
    }
    contentWindow.customElements.define('my-custom-element', MyCustomElement);

    var contentDocument = contentWindow.document;
    var instance = contentDocument.createElement('my-custom-element');
    var siblingBeforeInstance = contentDocument.createElement('b');
    var siblingAfterInstance = contentDocument.createElement('a');
    var container = contentDocument.createElement('div');
    container.appendChild(siblingBeforeInstance);
    container.appendChild(instance);
    container.appendChild(siblingAfterInstance);

    var containerClone = container.cloneNode(true);

    assert_equals(parentNodeInConstructor, containerClone,
        'An upgraded element must have its parentNode set before the custom element constructor is called');
    assert_equals(previousSiblingInConstructor, containerClone.firstChild,
        'An upgraded element must have its previousSibling set before the custom element constructor is called');
    assert_equals(nextSiblingInConstructor, containerClone.lastChild,
        'An upgraded element must have its nextSibling set before the custom element constructor is called');
}, 'Node.prototype.cloneNode(true) must set parentNode, previousSibling, and nextSibling before upgrading custom elements');

// The error reporting isn't clear yet when multiple globals involved in custom
// element, see w3c/webcomponents#635, so using test_with_window is not a good
// idea here.
test(function () {
    class MyCustomElement extends HTMLElement {
        constructor(doNotCreateItself) {
            super();
            if (!doNotCreateItself)
                new MyCustomElement(true);
        }
    }
    customElements.define('my-custom-element-constructed-after-super', MyCustomElement);

    var instance = new MyCustomElement(false);
    var uncaughtError;
    window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
    instance.cloneNode(false);
    assert_equals(uncaughtError.name, 'TypeError');
}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
    + ' due to a custom element constructor constructing itself after super() call');

test(function () {
    class MyCustomElement extends HTMLElement {
        constructor(doNotCreateItself) {
            if (!doNotCreateItself)
                new MyCustomElement(true);
            super();
        }
    }
    customElements.define('my-custom-element-constructed-before-super', MyCustomElement);

    var instance = new MyCustomElement(false);
    var uncaughtError;
    window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
    instance.cloneNode(false);
    assert_equals(uncaughtError.name, 'TypeError');
}, 'HTMLElement constructor must throw an TypeError when the top of the construction stack is marked AlreadyConstructed'
    + ' due to a custom element constructor constructing itself before super() call');

test(function () {
    var returnSpan = false;
    class MyCustomElement extends HTMLElement {
        constructor() {
            super();
            if (returnSpan)
                return document.createElement('span');
        }
    }
    customElements.define('my-custom-element-return-another', MyCustomElement);

    var instance = new MyCustomElement(false);
    returnSpan = true;
    var uncaughtError;
    window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
    instance.cloneNode(false);
    assert_equals(uncaughtError.name, 'TypeError');
}, 'Upgrading a custom element must throw TypeError when the custom element\'s constructor returns another element');

test(function () {
    var instance = document.createElement('my-custom-element-throw-exception');
    document.body.appendChild(instance);

    var calls = [];
    class MyCustomElement extends HTMLElement {
        constructor() {
            super();
            calls.push(this);
            throw 'bad';
        }
    }

    var uncaughtError;
    window.onerror = function (message, url, lineNumber, columnNumber, error) { uncaughtError = error; return true; }
    customElements.define('my-custom-element-throw-exception', MyCustomElement);
    assert_equals(uncaughtError, 'bad');

    assert_array_equals(calls, [instance]);
    document.body.removeChild(instance);
    document.body.appendChild(instance);
    assert_array_equals(calls, [instance]);
}, 'Inserting an element must not try to upgrade a custom element when it had already failed to upgrade once');

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