describe('ComponentBase', function() { async function importComponentBase(context) { const [Instrumentation, CommonComponentBase, ComponentBase] = await context.importScripts( ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js'], 'Instrumentation', 'CommonComponentBase', 'ComponentBase'); return ComponentBase; } function createTestToCheckExistenceOfShadowTree(callback, options = {htmlTemplate: false, cssTemplate: true}) { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { } if (options.htmlTemplate) SomeComponent.htmlTemplate = () => { return '
'; }; if (options.cssTemplate) SomeComponent.cssTemplate = () => { return ':host { height: 10px; }'; }; let instance = new SomeComponent('some-component'); instance.element().style.display = 'block'; context.document.body.appendChild(instance.element()); return callback(instance, () => { return instance.element().offsetHeight == 10; }); }); } it('must enqueue a connected component to render', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let renderCall = 0; class SomeComponent extends ComponentBase { render() { renderCall++; } } ComponentBase.defineElement('some-component', SomeComponent); let requestAnimationFrameCount = 0; let callback = null; context.global.requestAnimationFrame = (newCallback) => { callback = newCallback; requestAnimationFrameCount++; } expect(requestAnimationFrameCount).to.be(0); const instance = new SomeComponent; context.document.body.appendChild(instance.element()); expect(requestAnimationFrameCount).to.be(1); callback(); expect(renderCall).to.be(1); expect(requestAnimationFrameCount).to.be(1); }); }); it('must enqueue a connected component to render upon a resize event if enqueueToRenderOnResize is true', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { static get enqueueToRenderOnResize() { return true; } } ComponentBase.defineElement('some-component', SomeComponent); let requestAnimationFrameCount = 0; let callback = null; context.global.requestAnimationFrame = (newCallback) => { callback = newCallback; requestAnimationFrameCount++; } expect(requestAnimationFrameCount).to.be(0); const instance = new SomeComponent; context.global.dispatchEvent(new Event('resize')); context.document.body.appendChild(instance.element()); context.global.dispatchEvent(new Event('resize')); expect(requestAnimationFrameCount).to.be(1); }); }); it('must not enqueue a disconnected component to render upon a resize event if enqueueToRenderOnResize is true', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { static get enqueueToRenderOnResize() { return true; } } ComponentBase.defineElement('some-component', SomeComponent); let requestAnimationFrameCount = 0; let callback = null; context.global.requestAnimationFrame = (newCallback) => { callback = newCallback; requestAnimationFrameCount++; } const instance = new SomeComponent; expect(requestAnimationFrameCount).to.be(0); context.global.dispatchEvent(new Event('resize')); expect(requestAnimationFrameCount).to.be(0); }); }); describe('constructor', () => { it('is a function', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { expect(ComponentBase).to.be.a('function'); }); }); it('can be instantiated', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { let callCount = 0; class SomeComponent extends ComponentBase { constructor() { super('some-component'); callCount++; } } let instance = new SomeComponent; expect(instance).to.be.a(ComponentBase); expect(instance).to.be.a(SomeComponent); expect(callCount).to.be(1); }); }); it('must not create shadow tree eagerly', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { expect(hasShadowTree()).to.be(false); }); }); }); describe('element()', () => { it('must return an element', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { } let instance = new SomeComponent('some-component'); expect(instance.element()).to.be.a(context.global.HTMLElement); }); }); it('must return an element whose component() matches the component', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { } let instance = new SomeComponent('some-component'); expect(instance.element().component()).to.be(instance); }); }); it('must not create shadow tree eagerly', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.element(); expect(hasShadowTree()).to.be(false); }); }); }); describe('content()', () => { it('must create shadow tree', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.content(); expect(hasShadowTree()).to.be(true); }); }); it('must return the same shadow tree each time it is called', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { expect(instance.content()).to.be(instance.content()); }); }); it('must return the element matching the id if an id is specified', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { class SomeComponent extends ComponentBase { static htmlTemplate() { return '
'; } } ComponentBase.defineElement('some-component', SomeComponent); const instance = new SomeComponent; const part1 = instance.content('part1'); expect(part1.localName).to.be('div'); expect(part1.title).to.be('foo'); expect(instance.content('part2')).to.be(null); }); }); it('it must create DOM tree from contentTemplate', async () => { const context = new BrowsingContext(); const ComponentBase = await importComponentBase(context); class SomeComponent extends ComponentBase { }; SomeComponent.contentTemplate = ['div', {id: 'container'}, 'hello, world']; const instance = new SomeComponent('some-component'); const container = instance.content('container'); expect(container).to.be.a(context.global.HTMLDivElement); expect(container.textContent).to.be('hello, world'); }); it('it must create content using derived class content template', async () => { const context = new BrowsingContext(); const ComponentBase = await importComponentBase(context); class BaseClass extends ComponentBase { }; BaseClass.contentTemplate = ['div', {id: 'container'}, 'base-class']; const baseInstance = new BaseClass('base-class'); class DerivedClass extends BaseClass {}; DerivedClass.contentTemplate = ['div', {id: 'container'}, 'derived-class']; const derivedInstance = new DerivedClass('derived-class'); const baseContainer = baseInstance.content('container'); expect(baseContainer).to.be.a(context.global.HTMLDivElement); expect(baseContainer.textContent).to.be('base-class'); const derivedContainer = derivedInstance.content('container'); expect(derivedContainer).to.be.a(context.global.HTMLDivElement); expect(derivedContainer.textContent).to.be('derived-class'); }); it('it must create stylsheet from styleTemplate', async () => { const context = new BrowsingContext(); const ComponentBase = await importComponentBase(context); class SomeComponent extends ComponentBase { }; SomeComponent.contentTemplate = ['span', 'hello, world']; SomeComponent.styleTemplate = {':host': {'font-weight': 'bold'}}; const instance = new SomeComponent('some-component'); context.document.body.append(instance.element()); expect(context.global.getComputedStyle(instance.content().firstChild).fontWeight).to.be('bold'); }); }); describe('part()', () => { it('must create shadow tree', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.part('foo'); expect(hasShadowTree()).to.be(true); }); }); it('must return the component matching the id if an id is specified', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { class SomeComponent extends ComponentBase { } ComponentBase.defineElement('some-component', SomeComponent); class OtherComponent extends ComponentBase { static htmlTemplate() { return ''; } } ComponentBase.defineElement('other-component', OtherComponent); const otherComponent = new OtherComponent; const someComponent = otherComponent.part('foo'); expect(someComponent).to.be.a(SomeComponent); expect(someComponent.element().id).to.be('foo'); expect(otherComponent.part('foo')).to.be(someComponent); expect(otherComponent.part('bar')).to.be(null); }); }); }); describe('dispatchAction()', () => { it('must invoke a callback specified in listenToAction', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { class SomeComponent extends ComponentBase { } ComponentBase.defineElement('some-component', SomeComponent); const instance = new SomeComponent; const calls = []; instance.listenToAction('action', (...args) => { calls.push(args); }); const object = {'foo': 1}; instance.dispatchAction('action', 'bar', object, 5); expect(calls.length).to.be(1); expect(calls[0][0]).to.be('bar'); expect(calls[0][1]).to.be(object); expect(calls[0][2]).to.be(5); }); }); it('must not do anything when there are no callbacks', () => { return importComponentBase(new BrowsingContext()).then((ComponentBase) => { class SomeComponent extends ComponentBase { } ComponentBase.defineElement('some-component', SomeComponent); const object = {'foo': 1}; (new SomeComponent).dispatchAction('action', 'bar', object, 5); }); }); }); describe('enqueueToRender()', () => { it('must not immediately call render()', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { context.global.requestAnimationFrame = () => {} let renderCallCount = 0; const SomeComponent = class extends ComponentBase { render() { renderCallCount++; } } ComponentBase.defineElement('some-component', SomeComponent); (new SomeComponent).enqueueToRender(); expect(renderCallCount).to.be(0); (new SomeComponent).enqueueToRender(); expect(renderCallCount).to.be(0); }); }); it('must request an animation frame exactly once', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let requestAnimationFrameCount = 0; context.global.requestAnimationFrame = () => { requestAnimationFrameCount++; } const SomeComponent = class extends ComponentBase { } ComponentBase.defineElement('some-component', SomeComponent); expect(requestAnimationFrameCount).to.be(0); let instance = new SomeComponent; instance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); instance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); (new SomeComponent).enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); const AnotherComponent = class extends ComponentBase { } ComponentBase.defineElement('another-component', AnotherComponent); (new AnotherComponent).enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); }); }); it('must invoke render() when the callback to requestAnimationFrame is called', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let callback = null; context.global.requestAnimationFrame = (newCallback) => { expect(callback).to.be(null); expect(newCallback).to.not.be(null); callback = newCallback; } let renderCalls = []; const SomeComponent = class extends ComponentBase { render() { renderCalls.push(this); } } ComponentBase.defineElement('some-component', SomeComponent); expect(renderCalls.length).to.be(0); const instance = new SomeComponent; instance.enqueueToRender(); instance.enqueueToRender(); const anotherInstance = new SomeComponent; anotherInstance.enqueueToRender(); expect(renderCalls.length).to.be(0); callback(); expect(renderCalls.length).to.be(2); expect(renderCalls[0]).to.be(instance); expect(renderCalls[1]).to.be(anotherInstance); }); }); it('must immediately invoke render() on a component enqueued inside another render() call', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let callback = null; context.global.requestAnimationFrame = (newCallback) => { expect(callback).to.be(null); expect(newCallback).to.not.be(null); callback = newCallback; } let renderCalls = []; let instanceToEnqueue = null; const SomeComponent = class extends ComponentBase { render() { renderCalls.push(this); if (instanceToEnqueue) instanceToEnqueue.enqueueToRender(); instanceToEnqueue = null; } } ComponentBase.defineElement('some-component', SomeComponent); expect(renderCalls.length).to.be(0); const instance = new SomeComponent; const anotherInstance = new SomeComponent; instance.enqueueToRender(); instanceToEnqueue = anotherInstance; callback(); callback = null; expect(renderCalls.length).to.be(2); expect(renderCalls[0]).to.be(instance); expect(renderCalls[1]).to.be(anotherInstance); renderCalls = []; instance.enqueueToRender(); anotherInstance.enqueueToRender(); instanceToEnqueue = instance; callback(); expect(renderCalls.length).to.be(3); expect(renderCalls[0]).to.be(instance); expect(renderCalls[1]).to.be(anotherInstance); expect(renderCalls[2]).to.be(instance); }); }); it('must request a new animation frame once it exited the callback from requestAnimationFrame', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let requestAnimationFrameCount = 0; let callback = null; context.global.requestAnimationFrame = (newCallback) => { expect(callback).to.be(null); expect(newCallback).to.not.be(null); callback = newCallback; requestAnimationFrameCount++; } let renderCalls = []; const SomeComponent = class extends ComponentBase { render() { renderCalls.push(this); } } ComponentBase.defineElement('some-component', SomeComponent); const instance = new SomeComponent; const anotherInstance = new SomeComponent; expect(requestAnimationFrameCount).to.be(0); instance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); anotherInstance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(1); expect(renderCalls.length).to.be(0); callback(); callback = null; expect(renderCalls.length).to.be(2); expect(renderCalls[0]).to.be(instance); expect(renderCalls[1]).to.be(anotherInstance); expect(requestAnimationFrameCount).to.be(1); anotherInstance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(2); instance.enqueueToRender(); expect(requestAnimationFrameCount).to.be(2); expect(renderCalls.length).to.be(2); callback(); callback = null; expect(renderCalls.length).to.be(4); expect(renderCalls[0]).to.be(instance); expect(renderCalls[1]).to.be(anotherInstance); expect(renderCalls[2]).to.be(anotherInstance); expect(renderCalls[3]).to.be(instance); expect(requestAnimationFrameCount).to.be(2); }); }); }); describe('render()', () => { it('must create shadow tree', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.render(); expect(hasShadowTree()).to.be(true); }); }); it('must not create shadow tree when neither htmlTemplate nor cssTemplate are present', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.render(); expect(hasShadowTree()).to.be(false); }, {htmlTemplate: false, cssTemplate: false}); }); it('must create shadow tree when htmlTemplate is present and cssTemplate is not', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.render(); expect(hasShadowTree()).to.be(true); }, {htmlTemplate: true, cssTemplate: false}); }); it('must create shadow tree when cssTemplate is present and htmlTemplate is not', () => { return createTestToCheckExistenceOfShadowTree((instance, hasShadowTree) => { instance.render(); expect(hasShadowTree()).to.be(true); }, {htmlTemplate: false, cssTemplate: true}); }); it('must invoke didConstructShadowTree after creating the shadow tree', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let didConstructShadowTreeCount = 0; let htmlTemplateCount = 0; class SomeComponent extends ComponentBase { didConstructShadowTree() { expect(this.content()).to.be.a(context.global.ShadowRoot); didConstructShadowTreeCount++; } static htmlTemplate() { htmlTemplateCount++; return ''; } } ComponentBase.defineElement('some-component', SomeComponent); const instance = new SomeComponent; expect(didConstructShadowTreeCount).to.be(0); expect(htmlTemplateCount).to.be(0); instance.render(); expect(didConstructShadowTreeCount).to.be(1); expect(htmlTemplateCount).to.be(1); }); }); }); describe('createElement()', () => { it('should create an element of the specified name', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const div = ComponentBase.createElement('div'); expect(div).to.be.a(context.global.HTMLDivElement); }); }); it('should allow to create an element with attributes to be null or undefined', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const input = ComponentBase.createElement('input', {'title': null, 'required': undefined}); expect(input).to.be.a(context.global.HTMLInputElement); expect(input.attributes.length).to.be(2); expect(input.attributes[0].localName).to.be('title'); expect(input.attributes[0].value).to.be('null'); expect(input.attributes[1].localName).to.be('required'); expect(input.attributes[1].value).to.be('undefined'); }); }); it('should create an element with the specified attributes', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const input = ComponentBase.createElement('input', {'title': 'hi', 'id': 'foo', 'required': false, 'checked': true}); expect(input).to.be.a(context.global.HTMLInputElement); expect(input.attributes.length).to.be(3); expect(input.attributes[0].localName).to.be('title'); expect(input.attributes[0].value).to.be('hi'); expect(input.attributes[1].localName).to.be('id'); expect(input.attributes[1].value).to.be('foo'); expect(input.attributes[2].localName).to.be('checked'); expect(input.attributes[2].value).to.be(''); }); }); it('should create an element with the specified event handlers and attributes', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let clickCount = 0; const div = ComponentBase.createElement('div', {'title': 'hi', 'onclick': () => clickCount++}); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(1); expect(div.attributes[0].localName).to.be('title'); expect(div.attributes[0].value).to.be('hi'); expect(clickCount).to.be(0); div.click(); expect(clickCount).to.be(1); }); }); it('should create an element with the specified children when there is no attribute specified', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const element = ComponentBase.createElement; const span = element('span'); const div = element('div', [span, 'hi']); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(0); expect(div.childNodes.length).to.be(2); expect(div.childNodes[0]).to.be(span); expect(div.childNodes[1]).to.be.a(context.global.Text); expect(div.childNodes[1].data).to.be('hi'); }); }); it('should create an element with the specified children when the second argument is a span', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const element = ComponentBase.createElement; const span = element('span'); const div = element('div', span); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(0); expect(div.childNodes.length).to.be(1); expect(div.childNodes[0]).to.be(span); }); }); it('should create an element with the specified children when the second argument is a Text node', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const element = ComponentBase.createElement; const text = context.document.createTextNode('hi'); const div = element('div', text); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(0); expect(div.childNodes.length).to.be(1); expect(div.childNodes[0]).to.be(text); }); }); it('should create an element with the specified children when the second argument is a component', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { }; ComponentBase.defineElement('some-component', SomeComponent); const element = ComponentBase.createElement; const component = new SomeComponent; const div = element('div', component); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(0); expect(div.childNodes.length).to.be(1); expect(div.childNodes[0]).to.be(component.element()); }); }); it('should create an element with the specified attributes and children', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { const element = ComponentBase.createElement; const span = element('span'); const div = element('div', {'lang': 'en'}, [span, 'hi']); expect(div).to.be.a(context.global.HTMLDivElement); expect(div.attributes.length).to.be(1); expect(div.attributes[0].localName).to.be('lang'); expect(div.attributes[0].value).to.be('en'); expect(div.childNodes.length).to.be(2); expect(div.childNodes[0]).to.be(span); expect(div.childNodes[1]).to.be.a(context.global.Text); expect(div.childNodes[1].data).to.be('hi'); }); }); }); describe('defineElement()', () => { it('must define a custom element with a class of an appropriate name', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { class SomeComponent extends ComponentBase { } ComponentBase.defineElement('some-component', SomeComponent); let elementClass = context.global.customElements.get('some-component'); expect(elementClass).to.be.a('function'); expect(elementClass.name).to.be('SomeComponentElement'); }); }); it('must define a custom element that can be instantiated via document.createElement', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let instances = []; class SomeComponent extends ComponentBase { constructor() { super(); instances.push(this); } } ComponentBase.defineElement('some-component', SomeComponent); expect(instances.length).to.be(0); let element = context.document.createElement('some-component'); expect(instances.length).to.be(1); expect(element).to.be.a(context.global.HTMLElement); expect(element.component()).to.be(instances[0]); expect(instances[0].element()).to.be(element); expect(instances.length).to.be(1); }); }); it('must define a custom element that can be instantiated via new', () => { const context = new BrowsingContext(); return importComponentBase(context).then((ComponentBase) => { let instances = []; class SomeComponent extends ComponentBase { constructor() { super(); instances.push(this); } } ComponentBase.defineElement('some-component', SomeComponent); expect(instances.length).to.be(0); let component = new SomeComponent; expect(instances.length).to.be(1); expect(component).to.be(instances[0]); expect(component.element()).to.be.a(context.global.HTMLElement); expect(component.element().component()).to.be(component); expect(instances.length).to.be(1); }); }); }); describe('_ensureShadowTree', () => { it('should parse derived component after parsing base component', async () => { const context = new BrowsingContext(); const ComponentBase = await importComponentBase(context); class DerivedComponent extends ComponentBase {}; const baseInstance = new ComponentBase; expect(ComponentBase.hasOwnProperty('_parsed')).to.be(false); expect(DerivedComponent.hasOwnProperty('_parsed')).to.be(false); baseInstance._ensureShadowTree(); expect(ComponentBase.hasOwnProperty('_parsed')).to.be(true); expect(DerivedComponent.hasOwnProperty('_parsed')).to.be(false); expect(!!ComponentBase._parsed).to.be(true); expect(!!DerivedComponent._parsed).to.be(true); const derivedInstance = new DerivedComponent; derivedInstance._ensureShadowTree(); expect(ComponentBase.hasOwnProperty('_parsed')).to.be(true); expect(DerivedComponent.hasOwnProperty('_parsed')).to.be(true); expect(!!ComponentBase._parsed).to.be(true); expect(!!DerivedComponent._parsed).to.be(true); }); }); });