blob: e87b639df07b1dc3012381a44c57aa3a883d12ac [file] [log] [blame]
'use strict';
const assert = require('assert');
global.LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
global.CommonComponentBase = require('../public/shared/common-component-base.js').CommonComponentBase;
const MarkupComponentBase = require('../tools/js/markup-component.js').MarkupComponentBase;
describe('MarkupComponentBase', function () {
beforeEach(() => {
MarkupComponentBase.reset();
});
describe('constructor', function () {
it('should construct a component', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
assert.ok(component instanceof SomeComponent);
});
it('should throw if the component had not been defined', () => {
class SomeComponent extends MarkupComponentBase { };
assert.throws(() => new SomeComponent);
});
it('should throw if the component was defined with a different class (legacy named-based lookup should not be supported)', () => {
class SomeComponent extends MarkupComponentBase {
constructor() { super('some-component'); }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
class OtherComponent extends MarkupComponentBase {
constructor() { super('some-component'); }
};
assert.throws(() => new OtherComponent);
});
});
describe('defineElement', function () {
it('should throw if the component had already been defined', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
assert.throws(() => MarkupComponentBase.defineElement('some-component', SomeComponent));
});
it('should throw if the same class has already been used to define another component', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
assert.throws(() => MarkupComponentBase.defineElement('other-component', SomeComponent));
});
});
describe('element', function () {
it('should return a MarkupElement', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const element = component.element();
assert.ok(element);
assert.equal(element.__proto__.constructor.name, 'MarkupElement');
});
it('should return the same element each time called', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const element = component.element();
assert.equal(component.element(), element);
});
it('should return a different element for each instance', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component1 = new SomeComponent;
const component2 = new SomeComponent;
assert.notEqual(component1.element(), component2.element());
});
});
describe('content', function () {
it('should parse the content template once', () => {
class SomeComponent extends MarkupComponentBase { };
SomeComponent.contentTemplate = ['span', {'id': 'some'}, 'original'];
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance1 = new SomeComponent;
assert.equal(instance1.content('some').textContent, 'original');
SomeComponent.contentTemplate = ['span', {'id': 'some'}, 'modified'];
const instance2 = new SomeComponent;
assert.equal(instance2.content('some').textContent, 'original');
});
it('should upgrade components in the content tree', () => {
class SomeComponent extends MarkupComponentBase { };
SomeComponent.contentTemplate = ['span', ['other-component', {'id': 'other'}, 'hello']];
MarkupComponentBase.defineElement('some-component', SomeComponent);
class OtherComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('other-component', OtherComponent);
const someComponent = new SomeComponent;
const otherComponent = someComponent.content('other');
assert.equal(otherComponent.localName, 'other-component');
assert.equal(otherComponent.textContent, 'hello');
assert.ok(otherComponent.component() instanceof OtherComponent);
});
it('should upgrade components in the content tree in each instance', () => {
class SomeComponent extends MarkupComponentBase { };
SomeComponent.contentTemplate = ['span', ['other-component', {'id': 'other'}, 'hello']];
MarkupComponentBase.defineElement('some-component', SomeComponent);
let constructorCount = 0;
class OtherComponent extends MarkupComponentBase {
constructor(...args)
{
super(...args);
constructorCount++;
}
};
MarkupComponentBase.defineElement('other-component', OtherComponent);
assert.equal(constructorCount, 0);
const someComponent1 = new SomeComponent;
assert.ok(someComponent1.content('other').component() instanceof OtherComponent);
assert.equal(constructorCount, 1);
assert.ok(someComponent1.content('other').component() instanceof OtherComponent);
const someComponent2 = new SomeComponent;
assert.equal(constructorCount, 1);
assert.ok(someComponent2.content('other').component() instanceof OtherComponent);
assert.equal(constructorCount, 2);
});
it('should throw when the style template contains an unsupported selector', () => {
class SomeComponent extends MarkupComponentBase { };
SomeComponent.styleTemplate = {'div.target': {'font-weight': 'bold'}};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
assert.throws(() => component.content());
});
describe('without arguments', function () {
it('should return null when there are no templates', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
assert.equal(component.content(), null);
});
it('should return a MarkupContentRoot when there is a content template', () => {
class SomeComponent extends MarkupComponentBase {
static get contentTemplate() { return []; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const contentRoot = component.content();
assert.ok(contentRoot);
assert.equal(contentRoot.__proto__.constructor.name, 'MarkupContentRoot');
assert.deepEqual(contentRoot.childNodes, []);
});
});
describe('with an ID', () => {
it('should return null when there are no templates', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
assert.equal(component.content('some'), null);
});
it('should return null when there is a content template but no matching element', () => {
class SomeComponent extends MarkupComponentBase {
static get contentTemplate() { return ['span', {'id': 'other'}, 'hello world']; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const contentRoot = component.content();
assert.ok(contentRoot);
assert.equal(component.content('some'), null);
});
it('should return the matching element when there is one', () => {
class SomeComponent extends MarkupComponentBase {
static get contentTemplate() { return ['span', {'id': 'some'}, 'hello world']; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const contentRoot = component.content();
assert.ok(contentRoot);
const element = component.content('some');
assert.ok(element);
assert.equal(element.__proto__.constructor.name, 'MarkupElement');
assert.equal(element.id, 'some');
assert.equal(element.localName, 'span');
assert.equal(element.textContent, 'hello world');
});
it('should return the first matching element in the tree order', () => {
class SomeComponent extends MarkupComponentBase {
static get contentTemplate() { return [
['div', ['b', {'id': 'some'}, 'hello']],
['span', {'id': 'some'}, 'world'],
]; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
const contentRoot = component.content();
assert.ok(contentRoot);
const element = component.content('some');
assert.ok(element);
assert.equal(element.__proto__.constructor.name, 'MarkupElement');
assert.equal(element.id, 'some');
assert.equal(element.localName, 'b');
assert.equal(element.textContent, 'hello');
});
});
});
describe('enqueueRender', function () {
it('should enqueue the component to render', () => {
let renderCalls = 0;
class SomeComponent extends MarkupComponentBase {
render() { renderCalls++; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
component.enqueueToRender();
assert.equal(renderCalls, 0);
MarkupComponentBase.runRenderLoop();
assert.equal(renderCalls, 1);
});
it('should not enqueue the same component multiple times', () => {
let renderCalls = 0;
class SomeComponent extends MarkupComponentBase {
render() { renderCalls++; }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component = new SomeComponent;
component.enqueueToRender();
component.enqueueToRender();
assert.equal(renderCalls, 0);
MarkupComponentBase.runRenderLoop();
assert.equal(renderCalls, 1);
});
});
describe('runRenderLoop', function () {
it('should invoke render() on enqueued components in the oreder', () => {
let renderCalls = [];
class SomeComponent extends MarkupComponentBase {
render() { renderCalls.push(this); }
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const component1 = new SomeComponent;
const component2 = new SomeComponent;
component1.enqueueToRender();
component2.enqueueToRender();
assert.deepEqual(renderCalls, []);
MarkupComponentBase.runRenderLoop();
assert.deepEqual(renderCalls, [component1, component2]);
});
it('should process cascading calls to enqueueRender()', () => {
let renderCalls = [];
class SomeComponent extends MarkupComponentBase {
render() {
renderCalls.push(this);
if (this == instance1)
instance2.enqueueToRender();
}
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance1 = new SomeComponent;
const instance2 = new SomeComponent;
instance1.enqueueToRender();
assert.deepEqual(renderCalls, []);
MarkupComponentBase.runRenderLoop();
assert.deepEqual(renderCalls, [instance1, instance2]);
});
it('should delay render() call upon a cascading enqueuing', () => {
let renderCalls = [];
class SomeComponent extends MarkupComponentBase {
render() {
renderCalls.push(this);
if (this == instance1)
instance2.enqueueToRender();
}
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance1 = new SomeComponent;
const instance2 = new SomeComponent;
instance1.enqueueToRender();
instance2.enqueueToRender();
assert.deepEqual(renderCalls, []);
MarkupComponentBase.runRenderLoop();
assert.deepEqual(renderCalls, [instance1, instance2]);
});
it('should call render() again when a cascading enqueueing occurs after the initial call', () => {
let renderCalls = [];
class SomeComponent extends MarkupComponentBase {
render() {
renderCalls.push(this);
if (this == instance1)
instance2.enqueueToRender();
}
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance1 = new SomeComponent;
const instance2 = new SomeComponent;
instance2.enqueueToRender();
instance1.enqueueToRender();
assert.deepEqual(renderCalls, []);
MarkupComponentBase.runRenderLoop();
assert.deepEqual(renderCalls, [instance1, instance2, instance1]);
});
});
describe('renderReplace', function () {
it('should remove old children', () => {
class SomeComponent extends MarkupComponentBase {
render() {
const element = MarkupComponentBase.createElement;
this.renderReplace(this.content(), element('b', 'world'));
}
static get contentTemplate() {
return ['span', 'hello'];
}
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance = new SomeComponent;
let content = instance.content();
assert.equal(content.childNodes.length, 1);
assert.equal(content.childNodes[0].localName, 'span');
assert.equal(content.childNodes[0].textContent, 'hello');
instance.enqueueToRender();
MarkupComponentBase.runRenderLoop();
content = instance.content();
assert.equal(content.childNodes.length, 1);
assert.equal(content.childNodes[0].localName, 'b');
assert.equal(content.childNodes[0].textContent, 'world');
});
it('should insert the element of a component in the content tree', () => {
class SomeComponent extends MarkupComponentBase {
render() {
this.renderReplace(this.content(), new OtherComponent);
}
static get contentTemplate() {
return [];
}
};
MarkupComponentBase.defineElement('some-component', SomeComponent);
class OtherComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('other-component', OtherComponent);
const someComponent = new SomeComponent;
const content = someComponent.content();
assert.equal(content.childNodes.length, 0);
someComponent.enqueueToRender();
MarkupComponentBase.runRenderLoop();
assert.equal(content.childNodes.length, 1);
assert.equal(content.childNodes[0].localName, 'other-component');
assert.equal(content.childNodes[0].textContent, '');
const otherComponent = content.childNodes[0].component();
assert.ok(otherComponent instanceof OtherComponent);
});
it('should add classes to the generated elements if there are matching styles', () => {
class SomeComponent extends MarkupComponentBase {
render() {
this.renderReplace(this.content(), [
this.createElement('div'),
this.createElement('section', {class: 'target'}),
]);
}
};
SomeComponent.styleTemplate = {
'div': {'font-weight': 'bold'},
'.target': {'border': 'solid 1px blue'},
}
MarkupComponentBase.defineElement('some-component', SomeComponent);
const instance = new SomeComponent;
instance.enqueueToRender();
MarkupComponentBase.runRenderLoop();
const content = instance.content();
const div = content.childNodes[0];
const section = content.childNodes[1];
assert.equal(div.localName, 'div');
assert.ok(div.getAttribute('class'));
assert.equal(section.localName, 'section');
assert.ok(section.getAttribute('class').split(/\s+/).length, 2);
});
});
describe('createElement', function () {
it('should create an element of the specified name', () => {
const div = MarkupComponentBase.createElement('div');
assert.equal(div.localName, 'div');
assert.equal(div.__proto__.constructor.name, 'MarkupElement');
});
it('should create an element with the specified attributes', () => {
const input = MarkupComponentBase.createElement('input', {'title': 'hi', 'id': 'foo', 'required': false, 'checked': true});
assert.equal(input.localName, 'input');
assert.equal(input.attributes.length, 3);
assert.equal(input.attributes[0].localName, 'title');
assert.equal(input.attributes[0].value, 'hi');
assert.equal(input.attributes[1].localName, 'id');
assert.equal(input.attributes[1].value, 'foo');
assert.equal(input.attributes[2].localName, 'checked');
assert.equal(input.attributes[2].value, '');
});
it('should throw when an event handler is set', () => {
assert.throws(() => MarkupComponentBase.createElement('a', {'onclick': () => {}}));
});
it('should create an element with the specified children when the second argument is a span', () => {
const element = MarkupComponentBase.createElement;
const span = element('span');
const div = element('div', span);
assert.equal(div.attributes.length, 0);
assert.equal(div.childNodes.length, 1);
assert.equal(div.childNodes[0], span);
});
it('should create an element with the specified children when the second argument is a string', () => {
const element = MarkupComponentBase.createElement;
const div = element('div', 'hello');
assert.equal(div.attributes.length, 0);
assert.equal(div.childNodes.length, 1);
assert.equal(div.childNodes[0].__proto__.constructor.name, 'MarkupText');
assert.equal(div.childNodes[0].data, 'hello');
});
it('should create an element with the specified children when the second argument is a component', () => {
class SomeComponent extends MarkupComponentBase { };
MarkupComponentBase.defineElement('some-component', SomeComponent);
const element = MarkupComponentBase.createElement;
const component = new SomeComponent;
const div = element('div', component);
assert.equal(div.attributes.length, 0);
assert.equal(div.childNodes.length, 1);
assert.equal(div.childNodes[0], component.element());
});
it('should create an element with the specified attributes and children', () => {
const element = MarkupComponentBase.createElement;
const span = element('span');
const div = element('div', {'lang': 'en'}, [span, 'hi']);
assert.equal(div.localName, 'div');
assert.equal(div.attributes.length, 1);
assert.equal(div.attributes[0].localName, 'lang');
assert.equal(div.attributes[0].value, 'en');
assert.equal(div.childNodes.length, 2);
assert.equal(div.childNodes[0], span);
assert.equal(div.childNodes[1].data, 'hi');
});
});
describe('createLink', function () {
it('should create an anchor element', () => {
const anchor = MarkupComponentBase.createLink('hello', '#some-url');
assert.equal(anchor.localName, 'a');
assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
});
it('should create an anchor element with href and title when the second argument is a string and the third argument is ommitted', () => {
const anchor = MarkupComponentBase.createLink('hello', '#some-url');
assert.equal(anchor.localName, 'a');
assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
assert.equal(anchor.attributes.length, 2);
assert.equal(anchor.getAttribute('href'), '#some-url');
assert.equal(anchor.getAttribute('title'), 'hello');
assert.equal(anchor.textContent, 'hello');
});
it('should create an anchor element with href and title when the second and third arguments are string', () => {
const anchor = MarkupComponentBase.createLink('hello', 'some link', '#some-url');
assert.equal(anchor.localName, 'a');
assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
assert.equal(anchor.attributes.length, 2);
assert.equal(anchor.getAttribute('href'), '#some-url');
assert.equal(anchor.getAttribute('title'), 'some link');
assert.equal(anchor.textContent, 'hello');
});
it('should throw when the second argument is a function', () => {
assert.throws(() => MarkupComponentBase.createLink('hello', () => { }));
});
it('should throw when the third argument is a function', () => {
assert.throws(() => MarkupComponentBase.createLink('hello', 'some link', () => { }));
});
it('should create an anchor element with target=_blank when isExternal is true', () => {
const anchor = MarkupComponentBase.createLink('hello', 'some link', '#some-url', true);
assert.equal(anchor.localName, 'a');
assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
assert.equal(anchor.attributes.length, 3);
assert.equal(anchor.getAttribute('href'), '#some-url');
assert.equal(anchor.getAttribute('title'), 'some link');
assert.equal(anchor.getAttribute('target'), '_blank');
assert.equal(anchor.textContent, 'hello');
});
});
});