function setProto(A, B) { A.prototype = Object.create( B.prototype, {constructor: { configurable: true, writable: true, value: A }} ); } wru.test(typeof document === 'undefined' ? [] : [ { name: 'V1: main', test: function () { wru.assert(typeof customElements === 'object'); } }, { name: 'V1: is="x-name" is lower case', test: function () { function MyParagraph(self) { try { self = HTMLParagraphElement.call(self || this); } catch(e) { self = Reflect.construct(HTMLParagraphElement, [], MyParagraph); } return self; } setProto(MyParagraph, HTMLParagraphElement); customElements.define('my-paragraph', MyParagraph, {extends: 'p'}); var elem = new MyParagraph(); setTimeout(wru.async(function () { wru.assert('correct attribute', elem.getAttribute('is') === 'my-paragraph' || /^<\/p>$/i.test(elem.outerHTML) ); }), 50); } }, { name: 'V1: attributes are correctly notified', test: function () { function XTag(self) { try { self = HTMLElement.call(self || this); } catch(e) { self = Reflect.construct(HTMLElement, [], XTag); } return self; } setProto(XTag, HTMLElement); XTag.observedAttributes = ['x']; var calls = []; XTag.prototype.attributeChangedCallback = function (attr, old, value) { calls.push([attr, old, value]); }; document.body.appendChild( document.createElement('x-tag') ).setAttribute('x','1'); customElements.define('x-tag', XTag); var xtag = document.body.lastChild; xtag.setAttribute('x','2'); setTimeout(wru.async(function () { wru.assert('correct calls', calls.length === 2); wru.assert('correct first call', calls[0][0] == 'x' && calls[0][1] == null && calls[0][2] == '1'); wru.assert('correct second call', calls[1][0] == 'x' && calls[1][1] == '1' && calls[1][2] == '2'); }), 50); } }, { name: 'V1: use extended classes to register', test: function () { function MyButton(self) { // needed to upgrade the element try { self = HTMLButtonElement.call(self || this); } catch(e) { self = Reflect.construct(HTMLButtonElement, [], MyButton); } self.setAttribute('cool', 'true'); return self; } function method() {} setProto(MyButton, HTMLButtonElement); MyButton.prototype.method = method; customElements.define('my-button', MyButton, {'extends': 'button'}); var myButton = new MyButton(); wru.assert('prototype inherited', myButton.method === method); setTimeout(wru.async(function () { wru.assert('constructor called', myButton.getAttribute('cool') === 'true'); }), 100); } }, { name: 'V1: use extended to register and DOM to initialize', test: function () { var onceCreated = wru.async(function (myButton) { document.body.removeChild(myButton); wru.assert('constructor called', myButton.getAttribute('cool') === 'true'); wru.assert('prototype inherited', myButton.method === method); }); function MyOtherButton(self) { try { self = HTMLButtonElement.call(this, self); } catch(e) { self = Reflect.construct(HTMLButtonElement, [], MyOtherButton); } self.setAttribute('cool', 'true'); setTimeout(function () { onceCreated(self); }, 100); return self; } function method() {} setProto(MyOtherButton, HTMLButtonElement); MyOtherButton.prototype.method = method; customElements.define('my-other-button', MyOtherButton, {'extends': 'button'}); document.body.appendChild(document.createElement('button', {is: 'my-other-button'})); } }, { name: 'V1: real classes', test: function () { try { Function('wru', [ 'class MyA extends HTMLAnchorElement { constructor(self) { self = super(self); self.setAttribute("ok", "1"); return self; } }', 'customElements.define("my-a", MyA, {"extends": "a"});', 'const myA = new MyA();', 'setTimeout(wru.async(function(){ wru.assert(myA.getAttribute("ok") === "1"); }), 100);' ].join('\n')).call(this, wru); } catch(meh) {} } }, { name: 'V1: customElements.whenDefined', test: function () { function HereWeGo() {} setProto(HereWeGo, HTMLElement); customElements.whenDefined('here-we-go').then(wru.async(function () { wru.assert(customElements.get('here-we-go') === HereWeGo); })); setTimeout(function () { customElements.define('here-we-go', HereWeGo); }, 100); } }, { name: 'V1: connectedCallback', test: function () { function OnceAttached(self) { try { return HTMLElement.call(this, self); } catch(e) { return Reflect.construct(HTMLElement, [], OnceAttached); } } setProto(OnceAttached, HTMLElement); OnceAttached.prototype.connectedCallback = wru.async(function () { document.body.removeChild(this); wru.assert('OK'); }); customElements.define('once-attached', OnceAttached); var el = new OnceAttached; setTimeout(function () { document.body.appendChild(el); }, 100); } }, { name: 'V1: disconnectedCallback', test: function () { function OnceDetached(self) { try { return HTMLElement.call(this, self); } catch(e) { return Reflect.construct(HTMLElement, [], OnceDetached); } } setProto(OnceDetached, HTMLElement); OnceDetached.prototype.disconnectedCallback = wru.async(function () { wru.assert('OK'); }); customElements.define('once-detached', OnceDetached); var el = document.body.appendChild(new OnceDetached); setTimeout(function () { document.body.removeChild(el); }, 100); } }, { name: 'V1: attributeChangedCallback', test: function () { var args = []; function OnAttrModified(self) { try { return HTMLElement.call(this, self); } catch(e) { return Reflect.construct(HTMLElement, [], OnAttrModified); } } OnAttrModified.observedAttributes = ['test']; setProto(OnAttrModified, HTMLElement); OnAttrModified.prototype.attributeChangedCallback = function () { args.push(arguments); }; customElements.define('on-attr-modified', OnAttrModified); var el = document.body.appendChild(new OnAttrModified); el.setAttribute('nope', 'nope'); el.setAttribute('test', 'attr'); setTimeout(wru.async(function () { document.body.removeChild(el); wru.assert( args.length === 1 && args[0][0] === 'test' && args[0][1] == null && args[0][2] === 'attr' ); }), 100); } }, { name: 'V1: preserved instanceof', test: function () { wru.assert(document.createElement('button') instanceof HTMLButtonElement); } }, { name: 'V1: attributes notified on bootstrap', test: function () { var notification; function AttributesNotified(self) { try { return HTMLElement.call(this, self); } catch(e) { return Reflect.construct(HTMLElement, [], AttributesNotified); } } AttributesNotified.observedAttributes = ['some']; setProto(AttributesNotified, HTMLElement); AttributesNotified.prototype.attributeChangedCallback = function (name, oldValue, newValue) { notification = { name: name, oldValue: oldValue, newValue: newValue }; }; AttributesNotified.prototype.connectedCallback = wru.async(function () { this.parentNode.removeChild(this); wru.assert( notification.name === 'some' && notification.oldValue === null && notification.newValue === 'thing' ); }); setTimeout(function () { var div = document.body.appendChild(document.createElement('div')); div.innerHTML = 'test'; customElements.define('attributes-modified', AttributesNotified); }); } }, { name: "V0: attributeChangedCallback with empty values", test: function () { var done = wru.async(function (condition) { wru.assert(condition); }); document.registerElement( 'attr-changed', { prototype: Object.create( HTMLElement.prototype, { attributeChangedCallback: {value: function( name, // always present previousValue, // if null, it's a new attribute value // if null, it's a removed attribute ) { done( name === 'test' && previousValue === null && value === '' ); }} } ) }); var el = document.createElement('attr-changed'); document.body.appendChild(el).setAttribute('test', ''); } }, { name: "V0: main", test: function () { wru.assert('registerElement exists', document.registerElement); var XDirect = window.XDirect = document.registerElement( 'x-direct', { prototype: Object.create( HTMLElement.prototype, { createdCallback: {value: function() { this._info = [{ type: 'created', arguments: arguments }]; }}, attachedCallback: {value: function() { this._info.push({ type: 'attached', arguments: arguments }); }}, detachedCallback: {value: function() { this._info.push({ type: 'detached', arguments: arguments }); }}, attributeChangedCallback: {value: function( name, // always present previousValue, // if null, it's a new attribute value // if null, it's a removed attribute ) { this._info.push({ type: 'attributeChanged', arguments: arguments }); }} }) } ); var XIndirect = window.XIndirect = document.registerElement( 'x-indirect', { 'extends': 'div', prototype: Object.create( HTMLDivElement.prototype, { createdCallback: {value: function() { this._info = [{ type: 'created', arguments: arguments }]; }}, attachedCallback: {value: function() { this._info.push({ type: 'attached', arguments: arguments }); }}, detachedCallback: {value: function() { this._info.push({ type: 'detached', arguments: arguments }); }}, attributeChangedCallback: {value: function( name, // always present previousValue, // if null, it's a new attribute value // if null, it's a removed attribute ) { this._info.push({ type: 'attributeChanged', arguments: arguments }); }} }) } ); wru.assert('registerElement returns a function', typeof XDirect === 'function' && typeof XIndirect === 'function' ); } }, { name: 'V0: Observe changes when attached to V1 Shadow Root', test: function () { if(!HTMLElement.prototype.attachShadow) return; var a = new XDirect(), parentNode = document.createElement('div'), root = parentNode.attachShadow({ mode: 'open' }) ; root.appendChild(a); setTimeout(wru.async(function () { wru.assert('node created', a._info[0].type === 'created'); if (a._info[1]) wru.assert('node attached', a._info[1].type === 'attached'); }), 100); } }, { name: 'V0: as XDirect constructor', test: function () { var node = document.body.appendChild(new XDirect); wru.assert('right name', node.nodeName.toUpperCase() === 'X-DIRECT'); wru.assert('createdInvoked', node._info[0].type === 'created'); wru.assert('is instance', node instanceof XDirect || // IE < 11 where setPrototypeOf is absent Object.getPrototypeOf(XDirect.prototype).isPrototypeOf(node) ); } },{ name: 'V0: as XIndirect constructor', test: function () { var node = document.body.appendChild(new XIndirect); wru.assert('right name', node.nodeName.toUpperCase() === 'DIV'); wru.assert('right type', node.getAttribute('is') === 'x-indirect'); wru.assert('createdInvoked', node._info[0].type === 'created'); wru.assert('is instance', node instanceof XIndirect || // IE < 11 where setPrototypeOf is absent Object.getPrototypeOf(XIndirect.prototype).isPrototypeOf(node) ); } },{ name: 'V0: as <x-direct> innerHTML', test: function () { var node = document.body.appendChild(document.createElement('div')); node.innerHTML = ''; node = node.firstChild; wru.assert('right name', node.nodeName.toUpperCase() === 'X-DIRECT'); setTimeout(wru.async(function(){ wru.assert('created callback triggered', node._info[0].type === 'created'); wru.assert('attached callback triggered', node._info[1].type === 'attached'); document.body.removeChild(node.parentNode); setTimeout(wru.async(function(){ wru.assert('detached callback triggered', node._info[2].type === 'detached'); }), 20); }), 20); } },{ name: 'V0: as <div is="x-indirect"> innerHTML', test: function () { var node = document.body.appendChild(document.createElement('div')); node.innerHTML = '
'; node = node.firstChild; wru.assert('right name', node.nodeName.toUpperCase() === 'DIV'); wru.assert('right type', node.getAttribute('is') === 'x-indirect'); setTimeout(wru.async(function () { wru.assert('created callback triggered', node._info[0].type === 'created'); wru.assert('attached callback triggered', node._info[1].type === 'attached'); document.body.removeChild(node.parentNode); setTimeout(wru.async(function () { wru.assert('detached callback triggered', node._info[2].type === 'detached'); }), 20); }), 20); } },{ name: 'V0: as createElement(x-direct)', test: function () { var node = document.body.appendChild(document.createElement('div')).appendChild( document.createElement('x-direct') ); wru.assert('right name', node.nodeName.toUpperCase() === 'X-DIRECT'); setTimeout(wru.async(function () { wru.assert('created callback triggered', node._info[0].type === 'created'); wru.assert('attached callback triggered', node._info[1].type === 'attached'); document.body.removeChild(node.parentNode); setTimeout(wru.async(function () { wru.assert('detached callback triggered', node._info[2].type === 'detached'); }), 20); }), 20); } },{ name: 'V0: as createElement(div, x-indirect)', test: function () { var node = document.body.appendChild(document.createElement('div')).appendChild( document.createElement('div', 'x-indirect') ); wru.assert('right name', node.nodeName.toUpperCase() === 'DIV'); wru.assert('right type', node.getAttribute('is') === 'x-indirect'); setTimeout(wru.async(function () { wru.assert('created callback triggered', node._info[0].type === 'created'); wru.assert('attached callback triggered', node._info[1].type === 'attached'); document.body.removeChild(node.parentNode); setTimeout(wru.async(function () { wru.assert('detached callback triggered', node._info[2].type === 'detached'); }), 20); }), 20); } },{ name: 'V0: attributes', test: function () { var args, info; var node = document.createElement('x-direct'); document.body.appendChild(document.createElement('div')).appendChild(node); setTimeout(wru.async(function () { node.setAttribute('what', 'ever'); wru.assert(node.getAttribute('what') === 'ever'); setTimeout(wru.async(function () { info = node._info.pop(); wru.assert('attributeChanged was called', info.type === 'attributeChanged'); args = info.arguments; wru.assert('correct arguments with new value', args[0] === 'what' && args[1] == null && args[2] === 'ever'); node.setAttribute('what', 'else'); setTimeout(wru.async(function () { args = node._info.pop().arguments; wru.assert('correct arguments with old value', args[0] === 'what' && args[1] === 'ever' && args[2] === 'else'); node.removeAttribute('what'); setTimeout(wru.async(function () { args = node._info.pop().arguments; wru.assert( 'correct arguments with removed attribute', args[0] === 'what' && args[1] === 'else' && args[2] == null ); document.body.removeChild(node.parentNode); }), 20); }), 20); }), 20); }), 20); } },{ name: 'V0: offline', test: function () { var node = document.createElement('x-direct'); node.setAttribute('what', 'ever'); setTimeout(wru.async(function () { wru.assert('created callback triggered', node._info[0].type === 'created'); wru.assert('attributeChanged was called', node._info[1].type === 'attributeChanged'); args = node._info[1].arguments; wru.assert('correct arguments with new value', args[0] === 'what' && args[1] == null && args[2] === 'ever'); node.setAttribute('what', 'else'); setTimeout(wru.async(function () { args = node._info[2].arguments; wru.assert('correct arguments with old value', args[0] === 'what' && args[1] === 'ever' && args[2] === 'else'); node.removeAttribute('what'); setTimeout(wru.async(function () { args = node._info[3].arguments; wru.assert( 'correct arguments with removed attribute', args[0] === 'what' && args[1] === 'else' && args[2] == null ); }), 20); }), 20); }), 20); } },{ name: 'V0: nested', test: function () { var args, parentNode = document.createElement('div'), direct = parentNode.appendChild( document.createElement('x-direct') ), indirect = parentNode.appendChild( document.createElement('div', 'x-indirect') ), indirectNested = direct.appendChild( document.createElement('div', 'x-indirect') ) ; document.body.appendChild(parentNode); setTimeout(wru.async(function () { wru.assert('all node created', direct._info[0].type === 'created' && indirect._info[0].type === 'created' && indirectNested._info[0].type === 'created' ); wru.assert('all node attached', direct._info[1].type === 'attached' && indirect._info[1].type === 'attached' && indirectNested._info[1].type === 'attached' ); }), 20); } },{ name: 'V0: className', test: function () { // online for className, needed by IE8 var args, info, node = document.body.appendChild(document.createElement('x-direct')); setTimeout(wru.async(function () { node.className = 'a'; wru.assert(node.className === 'a'); setTimeout(wru.async(function () { info = node._info.pop(); wru.assert('attributeChanged was called', info.type === 'attributeChanged'); args = info.arguments; wru.assert('correct arguments with new value', args[0] === 'class' && args[1] == null && args[2] === 'a'); node.className += ' b'; setTimeout(wru.async(function () { info = node._info.pop(); // the only known device that fails this test is Blackberry 7 wru.assert('attributeChanged was called', info.type === 'attributeChanged'); args = info.arguments; wru.assert('correct arguments with new value', args[0] === 'class' && args[1] == 'a' && args[2] === 'a b'); }), 20); }), 20); }), 20); } },{ name: 'V0: registered after', test: function () { var node = document.body.appendChild( document.createElement('div') ), xd = node.appendChild(document.createElement('x-direct')), laterWeirdoElement = node.appendChild( document.createElement('later-weirdo') ), LaterWeirdo ; wru.assert('_info is not even present', !laterWeirdoElement._info); wru.assert('x-direct behaved regularly', xd._info); // now it's registered LaterWeirdo = document.registerElement( 'later-weirdo', { prototype: Object.create( HTMLElement.prototype, { createdCallback: {value: function() { this._info = [{ type: 'created', arguments: arguments }]; }}, attachedCallback: {value: function() { this._info.push({ type: 'attached', arguments: arguments }); }}, detachedCallback: {value: function() { this._info.push({ type: 'detached', arguments: arguments }); }}, attributeChangedCallback: {value: function( name, // always present previousValue, // if null, it's a new attribute value // if null, it's a removed attribute ) { this._info.push({ type: 'attributeChanged', arguments: arguments }); }} }) } ); // later on this should be setup setTimeout(wru.async(function(){ wru.assert('_info is now present', laterWeirdoElement._info); wru.assert('_info has right info', laterWeirdoElement._info.length === 2 && laterWeirdoElement._info[0].type === 'created' && laterWeirdoElement._info[1].type === 'attached'); wru.assert('xd has right info too', xd._info.length === 2 && xd._info[0].type === 'created' && xd._info[1].type === 'attached'); }), 100); } },{ name: 'V0: constructor', test: function () { var XEL, runtime, xEl = document.body.appendChild( document.createElement('x-el-created') ); XEL = document.registerElement( 'x-el-created', { prototype: Object.create( HTMLElement.prototype, { createdCallback: {value: function() { runtime = this.constructor; }} }) } ); setTimeout(wru.async(function () { wru.assert(xEl.constructor === runtime); // avoid IE8 problems if (!('attachEvent' in xEl)) { wru.assert(xEl instanceof XEL || // IE9 and IE10 will use HTMLUnknownElement // TODO: features tests/detection and use such prototype instead xEl instanceof HTMLUnknownElement); } }), 100); } },{ name: 'V0: simulating a table element', test: function () { if (window.HTMLTableElement && 'createCaption' in HTMLTableElement.prototype) { var HiTable = document.registerElement( 'hi-table', { 'extends': 'table', prototype: Object.create(HTMLTableElement.prototype) } ); var ht = document.createElement('table', 'hi-table'); wru.assert(!!ht.createCaption()); ht = new HiTable; wru.assert(!!ht.createCaption()); } } },{ name: 'V0: if registered one way, cannot be registered another way', test: function () { var failed = false; document.registerElement( 'no-double-behavior', {} ); try { document.registerElement( 'no-double-behavior', { 'extends': 'div' } ); } catch(e) { failed = true; } wru.assert('unable to register IS after TAG', failed); failed = false; document.registerElement( 'nope-double-behavior', { 'extends': 'div' } ); try { document.registerElement( 'nope-double-behavior', {} ); } catch(e) { failed = true; } wru.assert('unable to register TAG after IS', failed); } },{ name: 'V0: is="type" is a setup for known extends only', test: function () { var divTriggered = false; var spanTriggered = false; document.registerElement( 'did-trigger', { 'extends': 'div', prototype: Object.create( HTMLDivElement.prototype, { createdCallback: {value: function() { divTriggered = true; }} }) } ); document.registerElement( 'didnt-trigger', { 'extends': 'div', prototype: Object.create( HTMLDivElement.prototype, { createdCallback: {value: function() { spanTriggered = true; }} }) } ); var div = document.createElement('div', 'did-trigger'); var span = document.createElement('span', 'didnt-trigger'); setTimeout(wru.async(function () { wru.assert('it did trigger on div', divTriggered); wru.assert('but it did not trigger on span', !spanTriggered); }), 100); } }, { name: 'V0: nested CustomElement', test: function () { var a = new XDirect(); var b = new XDirect(); var div = document.createElement('div'); document.body.appendChild(div); b.appendChild(a); div.appendChild(b); setTimeout(wru.async(function () { wru.assert('there were info', a._info.length && b._info.length); a._info = []; b._info = []; a.setAttribute('what', 'ever'); setTimeout(wru.async(function () { wru.assert('attributeChanged triggered on a', a._info[0].type === 'attributeChanged' ); wru.assert('but it did not trigger on b', !b._info.length); }), 100); }), 100); } }, { name: 'V0: cannot extend a registered element', test: function () { var ok = false, ABC1 = document.registerElement('abc-1', { prototype: Object.create(HTMLElement.prototype) }); try { document.registerElement('abc-2', { 'extends': 'abc-1', prototype: Object.create(ABC1.prototype) }); } catch(e) { ok = true; } wru.assert('unable to create an element extending a custom one', ok); } }, { name: 'V0: do not invoke if attribute had same value', test: function () { var info = [], ChangingValue = document.registerElement('changing-value', { prototype: Object.create(HTMLElement.prototype, { attributeChangedCallback: {value: function( name, // always present previousValue, // if null, it's a new attribute value // if null, it's a removed attribute ) { info.push(arguments); }} }) }), node = document.body.appendChild(new ChangingValue); ; node.setAttribute('test', 'value'); setTimeout(wru.async(function(){ node.setAttribute('test', 'value'); wru.assert('OK'); setTimeout(wru.async(function () { wru.assert('was not called twice with the same value', info.length === 1 && info[0][0] === 'test' && info[0][1] === null && info[0][2] === 'value' ); }), 100); }), 100); } }, { name: 'V0: remove more than one CustomElement', test: function () { var a = new XDirect(); var b = new XDirect(); document.body.appendChild(a); document.body.appendChild(b); setTimeout(wru.async(function () { wru.assert('there were info', a._info.length && b._info.length); a._info = []; b._info = []; document.body.removeChild(a); document.body.removeChild(b); setTimeout(wru.async(function () { wru.assert('detachedCallback triggered on a', a._info[0].type === 'detached' ); wru.assert('detachedCallback triggered on b', b._info[0].type === 'detached' ); }), 100); }), 100); } } ]);