Skip to content

Commit

Permalink
Tagcloud fixes (elastic#9194)
Browse files Browse the repository at this point in the history
* Improve robustness

Tagcloud was subsceptible to race conditions, potentially yielding stale clouds.
- Introduced queue to explicitly keep track of outstanding jobs and avoid clobbering of the state.
- Expanded unit tests

* ensure tagcloud expands to full height in reporting

* remove magic numbers
  • Loading branch information
thomasneirynck authored and epixa committed Nov 29, 2016
1 parent 10eb827 commit 8a528dc
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 96 deletions.
217 changes: 183 additions & 34 deletions src/core_plugins/tagcloud/public/__tests__/tag_cloud.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import expect from 'expect.js';
import _ from 'lodash';
import TagCloud from 'plugins/tagcloud/tag_cloud';
import d3 from 'd3';

describe('tag cloud', function () {

Expand All @@ -22,11 +23,14 @@ describe('tag cloud', function () {
});


const baseTestConfig = {
const minValue = 1;
const maxValue = 9;
const midValue = (minValue + maxValue) / 2;
const baseTest = {
data: [
{text: 'foo', size: 1},
{text: 'bar', size: 5},
{text: 'foobar', size: 9},
{text: 'foo', value: minValue},
{text: 'bar', value: midValue},
{text: 'foobar', value: maxValue},
],
options: {
orientation: 'single',
Expand All @@ -50,31 +54,47 @@ describe('tag cloud', function () {
]
};

const singleLayout = _.cloneDeep(baseTestConfig);
const rightAngleLayout = _.cloneDeep(baseTestConfig);
rightAngleLayout.options.orientation = 'right angled';
const multiLayout = _.cloneDeep(baseTestConfig);
multiLayout.options.orientation = 'multiple';
const logScale = _.cloneDeep(baseTestConfig);
logScale.options.scale = 'log';
logScale.expected[1].fontSize = '31px';
const sqrtScale = _.cloneDeep(baseTestConfig);
sqrtScale.options.scale = 'square root';
sqrtScale.expected[1].fontSize = '27px';
const biggerFont = _.cloneDeep(baseTestConfig);
biggerFont.options.minFontSize = 36;
biggerFont.options.maxFontSize = 72;
biggerFont.expected[0].fontSize = '36px';
biggerFont.expected[1].fontSize = '54px';
biggerFont.expected[2].fontSize = '72px';
const singleLayoutTest = _.cloneDeep(baseTest);

const rightAngleLayoutTest = _.cloneDeep(baseTest);
rightAngleLayoutTest.options.orientation = 'right angled';

const multiLayoutTest = _.cloneDeep(baseTest);
multiLayoutTest.options.orientation = 'multiple';

const mapWithLog = d3.scale.log();
mapWithLog.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
mapWithLog.domain([minValue, maxValue]);
const logScaleTest = _.cloneDeep(baseTest);
logScaleTest.options.scale = 'log';
logScaleTest.expected[1].fontSize = Math.round(mapWithLog(midValue)) + 'px';

const mapWithSqrt = d3.scale.sqrt();
mapWithSqrt.range([baseTest.options.minFontSize, baseTest.options.maxFontSize]);
mapWithSqrt.domain([minValue, maxValue]);
const sqrtScaleTest = _.cloneDeep(baseTest);
sqrtScaleTest.options.scale = 'square root';
sqrtScaleTest.expected[1].fontSize = Math.round(mapWithSqrt(midValue)) + 'px';

const biggerFontTest = _.cloneDeep(baseTest);
biggerFontTest.options.minFontSize = 36;
biggerFontTest.options.maxFontSize = 72;
biggerFontTest.expected[0].fontSize = '36px';
biggerFontTest.expected[1].fontSize = '54px';
biggerFontTest.expected[2].fontSize = '72px';

const trimDataTest = _.cloneDeep(baseTest);
trimDataTest.data.splice(1, 1);
trimDataTest.expected.splice(1, 1);

[
singleLayout,
rightAngleLayout,
multiLayout,
logScale,
sqrtScale,
biggerFont
singleLayoutTest,
rightAngleLayoutTest,
multiLayoutTest,
logScaleTest,
sqrtScaleTest,
biggerFontTest,
trimDataTest
].forEach((test, index) => {

it(`should position elements correctly: ${index}`, done => {
Expand All @@ -91,14 +111,143 @@ describe('tag cloud', function () {
});
});

it(`should not put elements in view when container to small`, function (done) {

it('should use the latest state before notifying (modifying options)', function (done) {

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.setOptions(logScaleTest.options);
tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
const textElements = domNode.querySelectorAll('text');
verifyTagProperties(logScaleTest.expected, textElements);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
done();
});

});


it('should use the latest state before notifying (modifying data)', function (done) {

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.setData(trimDataTest.data);

tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
const textElements = domNode.querySelectorAll('text');
verifyTagProperties(trimDataTest.expected, textElements);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
done();
});

});

[
5,
100,
200,
300,
500
].forEach(function (timeout, index) {
it(`should only send single renderComplete event: ${index}`, function (done) {
//TagCloud takes at least 600ms to complete (due to d3 animation)
//renderComplete should only notify at the last one
const tagCloud = new TagCloud(domNode);
tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
const textElements = domNode.querySelectorAll('text');
verifyTagProperties(logScaleTest.expected, textElements);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
done();
});
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
setTimeout(() => {
tagCloud.setOptions(logScaleTest.options);
}, timeout);
});
});

it('should not get multiple render-events', function (done) {

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);

setTimeout(() => {
//this should be overridden by later changes
tagCloud.setData(sqrtScaleTest.data);
tagCloud.setOptions(sqrtScaleTest.options);
}, 100);

setTimeout(() => {
tagCloud.setData(logScaleTest.data);
tagCloud.setOptions(logScaleTest.options);
}, 300);

let counter = 0;

function onRender() {
if (counter > 0) {
throw new Error('Should not get multiple render events');
}
counter += 1;
const textElements = domNode.querySelectorAll('text');
verifyTagProperties(logScaleTest.expected, textElements);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
}

tagCloud.on('renderComplete', onRender);
setTimeout(function () {
tagCloud.removeListener('renderComplete', onRender);
done();
}, 1500);

});

it('should show correct data when state-updates are interleaved with resize event', function (done) {

const tagCloud = new TagCloud(domNode);
tagCloud.setData(logScaleTest.data);
tagCloud.setOptions(logScaleTest.options);

setTimeout(() => {
domNode.style.width = '600px';
domNode.style.height = '600px';

tagCloud.resize();

setTimeout(() => {
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
}, 200);


tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
const textElements = domNode.querySelectorAll('text');
verifyTagProperties(baseTest.expected, textElements);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
done();
});
}, 1000);


});


it(`should not put elements in view when container is too small`, function (done) {

domNode.style.width = '1px';
domNode.style.height = '1px';

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTestConfig.data);
tagCloud.setOptions(baseTestConfig.options);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE);
Expand All @@ -118,8 +267,8 @@ describe('tag cloud', function () {
domNode.style.height = '1px';

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTestConfig.data);
tagCloud.setOptions(baseTestConfig.options);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.INCOMPLETE);
Expand All @@ -138,8 +287,8 @@ describe('tag cloud', function () {
it(`tags should no longer fit after making container smaller`, function (done) {

const tagCloud = new TagCloud(domNode);
tagCloud.setData(baseTestConfig.data);
tagCloud.setOptions(baseTestConfig.options);
tagCloud.setData(baseTest.data);
tagCloud.setOptions(baseTest.options);
tagCloud.on('renderComplete', function onRender() {
tagCloud.removeListener('renderComplete', onRender);
expect(tagCloud.getStatus()).to.equal(TagCloud.STATUS.COMPLETE);
Expand Down
Loading

0 comments on commit 8a528dc

Please sign in to comment.