blob: 4418f81997a7d72ffe690d5f40ce0c77eaf4d778 [file] [log] [blame]
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 '<div id="div" style="height: 10px;"></div>'; };
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 '<div id="part1" title="foo"></div><div id="part1"></div>'; }
}
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 '<some-component id="foo"></some-component>'; }
}
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);
});
});
});