Home Reference Source

src/utils/utils.spec.js

import { expect, assert } from 'chai';
import _ from 'lodash';
import writtenNumber from 'written-number';
import utils from '.';
import components from './fixtures/components.json';
import submission1 from './fixtures/submission1.json';

describe('eachComponent', () => {
  it('should iterate through nested components in the right order', () => {
    let n = 1;
    utils.eachComponent(components, (component) => {
      expect(component.order).to.equal(n);
      n += 1;
    });
  });

  it('should include layouts components if provided', () => {
    let numComps = 0;
    let numLayout = 0;
    utils.eachComponent(components, (component) => {
      if (utils.isLayoutComponent(component)) {
        numLayout++;
      }
      else {
        numComps++;
      }
    }, true);
    expect(numLayout).to.be.equal(3);
    expect(numComps).to.be.equal(8);
  });

  it('Should provide the paths to all of the components', () => {
    const paths = [
      'one',
      'parent1',
      'two',
      'parent2',
      'three',
      '',
      'four',
      'five',
      'six',
      'seven',
      'eight'
    ];
    const testPaths = [];
    utils.eachComponent(components, (component, path) => {
      testPaths.push(path);
    }, true);
    expect(paths).to.deep.equal(testPaths);
  });

  it('Should be able to find all textfield components', () => {
    const comps = utils.findComponents(components, { type: 'textfield' });
    expect(comps.length).to.equal(6);
  });

  it('Should be able to find components with special properties.', () => {
    const components3 = require('./fixtures/components3.json');
    const comps = utils.findComponents(components3, { 'properties.path': 'a' });
    expect(comps.length).to.equal(4);
    expect(comps[0].key).to.equal('b');
    expect(comps[1].key).to.equal('e');
    expect(comps[2].key).to.equal('j');
    expect(comps[3].key).to.equal('m');
  });

  it('Should be able to generate paths based on component types', () => {
    const components = require('./fixtures/components2.json');
    const paths = [
      'a',
      'b',
      'c',
      'd',
      'f',
      'f.g',
      'f.h',
      'f.i',
      'e',
      'j',
      'k',
      'k.n',
      'k.n.o',
      'k.n.p',
      'k.n.q',
      'k.m',
      'k.l',
      'r',
      'submit'
    ];
    const testPaths = [];
    utils.eachComponent(components, (component, path) => {
      testPaths.push(path);
    }, true);
    expect(paths).to.deep.equal(testPaths);
  });

  it('Should still provide the correct paths when it is not recursive', () => {
    const paths = [
      'a',
      'd',
      'f',
      'f.g',
      'f.h',
      'f.i',
      'e',
      'j',
      'k',
      'k.n',
      'k.n.o',
      'k.n.p',
      'k.n.q',
      'k.m',
      'k.l',
      'r',
      'submit'
    ];
    const testPaths = [];
    utils.eachComponent(require('./fixtures/components2.json'), (component, path) => {
      testPaths.push(path);
    });
    expect(paths).to.deep.equal(testPaths);
  });

  it('should be able to block recursion', () => {
    let numComps = 0;
    let numLayout = 0;
    utils.eachComponent(components, (component) => {
      if (utils.isLayoutComponent(component)) {
        numLayout++;
      }
      else {
        numComps++;
      }

      if (component.type === 'table') {
        let numInTable = 0;
        [].concat.apply([], component.rows).forEach((row) => {
          utils.eachComponent(row.components, () => {
            numInTable++;
          });
        });
        expect(numInTable).to.be.equal(4);
        return true;
      }
    }, true);
    expect(numLayout).to.be.equal(3);
    expect(numComps).to.be.equal(4);
  });
});

describe('getComponent', () => {
  it('should return the correct components', () => {
    for (let n = 1; n <= 8; n += 1) {
      const component = utils.getComponent(components, writtenNumber(n));
      expect(component).not.to.be.null;
      expect(component).not.to.be.undefined;
      expect(component).to.be.an('object');
      expect(component.order).to.equal(n);
      expect(component.key).to.equal(writtenNumber(n));
    }
  });

  it('should work with a different this context', () => {
    for (let n = 1; n <= 8; n += 1) {
      const component = utils.getComponent.call({}, components, writtenNumber(n));
      expect(component).not.to.be.null;
      expect(component).not.to.be.undefined;
      expect(component).to.be.an('object');
      expect(component.order).to.equal(n);
      expect(component.key).to.equal(writtenNumber(n));
    }
  });
});

describe('flattenComponents', () => {
  it('should return an object of flattened components', () => {
    const flattened = utils.flattenComponents(components);
    for (let n = 1; n <= 8; n += 1) {
      const component = flattened[writtenNumber(n)];
      expect(component).not.to.be.undefined;
      expect(component).to.be.an('object');
      expect(component.order).to.equal(n);
      expect(component.key).to.equal(writtenNumber(n));
    }
  });

  it('should work with a different this context', () => {
    const flattened = utils.flattenComponents.call({}, components);
    for (let n = 1; n <= 8; n += 1) {
      const component = flattened[writtenNumber(n)];
      expect(component).not.to.be.undefined;
      expect(component).to.be.an('object');
      expect(component.order).to.equal(n);
      expect(component.key).to.equal(writtenNumber(n));
    }
  });
});

describe('getValue', () => {
  it('should be able to get a simple value', () => {
    expect(utils.getValue(submission1, 'name')).to.be.equal(submission1.data.name);
  });

  it('should be able to get a value from a container', () => {
    expect(utils.getValue(submission1, 'animalname')).to.be.equal(submission1.data.mycontainer.animalname);
  });
});

describe('parseFloat', () => {
  it('should clear input and parse value', () => {
    expect(utils.parseFloatExt('12,345,678.90')).to.be.equal(12345678.90);
  });
});

describe('formatAsCurrency', () => {
  it('should be able to format Float value for Currency component', () => {
    expect(utils.formatAsCurrency(123.4)).to.be.equal('123.40');
    expect(utils.formatAsCurrency(12345678.9)).to.be.equal('12,345,678.90');
    expect(utils.formatAsCurrency(12345678.915)).to.be.equal('12,345,678.92');
  });

  it('should be able to format String value for Currency component', () => {
    expect(utils.formatAsCurrency('12345678.915')).to.be.equal('12,345,678.92');
  });
});

describe('checkCalculated', () => {
  it('should be able to calculate value based on javascript code', () => {
    const component = {
      key: 'sum',
      calculateValue: 'value = 3'
    };
    const data = {};

    utils.checkCalculated(component, null, data);
    expect(data.sum).to.be.equal(3);
  });

  it('should be able to calculate value based on json logic', () => {
    const component = {
      key: 'sum',
      calculateValue: {
        '_sum': { var: 'data.test' }
      }
    };
    const data = { test: [1, 2, 3] };

    utils.checkCalculated(component, null, data);
    expect(data.sum).to.be.equal(6);
  });

  it('should return undefined if no logic provided', () => {
    const component = {
      key: 'sum',
      calculateValue: '/* do nothing */'
    };
    const data = {};

    utils.checkCalculated(component, null, data);
    expect(data.sum).to.be.undefined;
  });
});

describe('checkCondition', () => {
  it('should display component by default', () => {
    expect(utils.checkCondition({}, null, {})).to.be.equal(true);
  });

  it('should calculate simple triggers', () => {
    const component = {
      key: 'sum',
      conditional: {
        when: 'test',
        eq: 3,
        show: true
      }
    };
    const data1 = { test: 3 };
    const data2 = { test: 5 };
    expect(utils.checkCondition(component, null, data1)).to.be.equal(true);
    expect(utils.checkCondition(component, null, data2)).to.be.equal(false);
  });

  it('should be able to calculate condition based on javascript code', () => {
    const component = {
      key: 'sum',
      customConditional(context) {
        return context.data.test === 3;
      }
    };
    const data1 = { test: 3 };
    const data2 = { test: 5 };

    expect(utils.checkCondition(component, null, data1)).to.be.equal(true);
    expect(utils.checkCondition(component, null, data2)).to.be.equal(false);
  });

  it('should be able to calculate condition based on json logic', () => {
    const component = {
      key: 'sum',
      conditional: {
        json: {
          '===': [
            { '_sum': { var: 'data.test' } },
            6
          ]
        }
      }
    };
    const data1 = { test: [1, 2, 3] };
    const data2 = { test: [1, 2, 4] };

    expect(utils.checkCondition(component, null, data1)).to.be.equal(true);
    expect(utils.checkCondition(component, null, data2)).to.be.equal(false);
  });
});

describe('getDateSetting', () => {
  it('should return null if no date provided', () => {
    expect(utils.getDateSetting()).to.be.equal(null);
    expect(utils.getDateSetting(null)).to.be.equal(null);
    expect(utils.getDateSetting(undefined)).to.be.equal(null);
    expect(utils.getDateSetting(NaN)).to.be.equal(null);
    expect(utils.getDateSetting('')).to.be.equal(null);
    expect(utils.getDateSetting('should be invalid')).to.be.equal(null);
  });

  it('should return valid Date on serialized date provided', () => {
    const date = new Date(0);
    expect(utils.getDateSetting(date)).to.be.eql(date);
    expect(utils.getDateSetting(date.valueOf())).to.be.eql(date);
    expect(utils.getDateSetting(date.toString())).to.be.eql(date);
    expect(utils.getDateSetting(date.toISOString())).to.be.eql(date);
  });

  it('should be able to get value using moment APIs', () => {
    const validMomentExpression = 'moment(0)';
    const validDate = new Date(0);
    const invalidMomentExpression = "moment('')";

    expect(utils.getDateSetting(validMomentExpression)).to.be.eql(validDate);
    expect(utils.getDateSetting(invalidMomentExpression)).to.be.equal(null);
  });
});

describe('checkTrigger', () => {
  it('should default to false', () => {
    expect(utils.checkCondition({}, { type: 'none' }, null, {})).to.be.equal(true);
  });

  it('should calculate simple triggers', () => {
    const component = {
      key: 'sum'
    };
    const trigger = {
      type: 'simple',
      simple: {
        when: 'test',
        eq: 3,
        show: true
      }
    };
    const data1 = { test: 3 };
    const data2 = { test: 5 };
    expect(utils.checkTrigger(component, trigger, null, data1)).to.be.equal(true);
    expect(utils.checkTrigger(component, trigger, null, data2)).to.be.equal(false);
  });

  it('should be able to calculate trigger based on javascript code', () => {
    const component = {
      key: 'sum'
    };
    const trigger = {
      type: 'javascript',
      javascript: 'result = data.test === 3'
    };
    const data1 = { test: 3 };
    const data2 = { test: 5 };

    expect(utils.checkTrigger(component, trigger, null, data1)).to.be.equal(true);
    expect(utils.checkTrigger(component, trigger, null, data2)).to.be.equal(false);
  });

  it('should be able to calculate trigger based on json logic', () => {
    const component = {
      key: 'sum'
    };
    const trigger = {
      type: 'json',
      json: {
        '===': [
          { '_sum': { var: 'data.test' } },
          6
        ]
      }
    };
    const data1 = { test: [1, 2, 3] };
    const data2 = { test: [1, 2, 4] };

    expect(utils.checkTrigger(component, trigger, null, data1)).to.be.equal(true);
    expect(utils.checkTrigger(component, trigger, null, data2)).to.be.equal(false);
  });
});

describe('setActionProperty', () => {
  it('should set a boolean action property to true', () => {
    const component = {
      key: 'test',
      disabled: false
    };
    const action = {
      type: 'property',
      property: {
        label: 'Disabled',
        value: 'disabled',
        type: 'boolean'
      },
      state: true
    };
    utils.setActionProperty(component, action, {}, {}, true);
    expect(component.disabled).to.be.equal(true);
  });

  it('should set a boolean action property to false', () => {
    const component = {
      key: 'test',
      disabled: true
    };
    const action = {
      type: 'property',
      property: {
        label: 'Disabled',
        value: 'disabled',
        type: 'boolean'
      },
      state: false
    };
    utils.setActionProperty(component, action, {}, {}, true);
    expect(component.disabled).to.be.equal(false);
  });

  it('should set a boolean action nested property', () => {
    const component = {
      key: 'test',
      validate: {
        required: true
      }
    };
    const action = {
      type: 'property',
      property: {
        label: 'Required',
        value: 'validate.required',
        type: 'boolean'
      },
      state: false
    };
    utils.setActionProperty(component, action, {}, {}, true);
    expect(component.validate.required).to.be.equal(false);
  });

  it('should set a string action property', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const action = {
      type: 'property',
      property: {
        label: 'Label',
        value: 'label',
        type: 'string'
      },
      text: 'bar'
    };
    utils.setActionProperty(component, action, {}, {}, true);
    expect(component.label).to.be.equal('bar');
  });

  it('should set a string action property with row templating', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const action = {
      type: 'property',
      property: {
        label: 'Label',
        value: 'label',
        type: 'string'
      },
      text: 'bar {{ row.field }}'
    };
    utils.setActionProperty(component, action, { field: 'baz' }, {}, true);
    expect(component.label).to.be.equal('bar baz');
  });

  it('should set a string action property with data templating', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const action = {
      type: 'property',
      property: {
        label: 'Label',
        value: 'label',
        type: 'string'
      },
      text: 'bar {{ data.field }}'
    };
    utils.setActionProperty(component, action, {}, { field: 'baz' }, true);
    expect(component.label).to.be.equal('bar baz');
  });

  it('should set a string action property with result templating', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const action = {
      type: 'property',
      property: {
        label: 'Label',
        value: 'label',
        type: 'string'
      },
      text: 'bar {{ result }}'
    };
    utils.setActionProperty(component, action, {}, {}, 'baz');
    expect(component.label).to.be.equal('bar baz');
  });

  it('should set a string action property with component templating', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const action = {
      type: 'property',
      property: {
        label: 'Label',
        value: 'label',
        type: 'string'
      },
      text: 'bar {{ component.key }}'
    };
    utils.setActionProperty(component, action, {}, {}, 'baz');
    expect(component.label).to.be.equal('bar test');
  });

  it('should do nothing with a bad request', () => {
    const component = {
      key: 'test',
      label: 'foo'
    };
    const originalComponent = _.cloneDeep(component);
    expect(component).to.deep.equal(originalComponent);
  });
});

describe('delay', () => {
  let score = 0;

  function incScore(value) {
    score += value || 1;
  }

  beforeEach(() => {
    score = 0;
  });

  it('should act as regular setTimeout()', (done) => {
    utils.delay(incScore);
    utils.delay(incScore, 0);
    utils.delay(incScore, 100, 2);
    utils.delay(() => {
      if (score === 4) {
        done();
      }
    }, 200);
  });

  it('should be cancelable via direct timer access', (done) => {
    const delay = utils.delay(incScore);
    clearTimeout(delay.timer);
    setTimeout(() => {
      if (score === 0) {
        done();
      }
    }, 100);
  });

  it('should be cancelable via cancel() method', (done) => {
    const delay = utils.delay(incScore);
    delay.cancel();
    setTimeout(() => {
      if (score === 0) {
        done();
      }
    }, 100);
  });

  it('should be able to call passed function synchronously', (done) => {
    const delay = utils.delay(incScore);
    delay();
    if (score === 1) {
      done();
    }
  });
});

describe('withSwitch', () => {
  it('should return Array with two functions', () => {
    const fns = utils.withSwitch();

    expect(fns).to.be.an('array').and.have.lengthOf(2);
    expect(fns[0]).to.be.a('function');
    expect(fns[1]).to.be.a('function');
  });

  describe('#get', () => {
    it('should return one of state', () => {
      const [get] = utils.withSwitch(42, 24);
      expect(get()).to.be.equal(42);
    });

    it('should be pure', () => {
      const [get] = utils.withSwitch(42, 24);
      expect(get()).to.be.equal(42);
      expect(get()).to.be.equal(42);
      expect(get()).to.be.equal(42);
      expect(get()).to.be.equal(42);
    });
  });

  describe('#toggle', () => {
    it('should cycle between states', () => {
      const [get, toggle] = utils.withSwitch(42, 24);
      expect(get()).to.be.equal(42);
      toggle();
      expect(get()).to.be.equal(24);
      toggle();
      expect(get()).to.be.equal(42);
    });
  });
});

describe('unfold', () => {
  it('should return provided argument', () => {
    const parameters = [{}, 1, null, 'string'];

    parameters.forEach(p => {
      assert(p === utils.unfold(p));
    });
  });

  it('should call parameter, if it is function and return result', () => {
    const x = Symbol('__unfold__');
    assert(utils.unfold(() => x) === x);
  });
});

describe('firstNonNil', () => {
  it('should return first non nil value', () => {
    expect(utils.firstNonNil([1])).to.equal(1);
    expect(utils.firstNonNil([1, 3])).to.equal(1);
    expect(utils.firstNonNil([3, 2, 1])).to.equal(3);
    expect(utils.firstNonNil([undefined, undefined, 3, 1])).to.equal(3);
  });
  it('should unfold all functions in array', () => {
    expect(utils.firstNonNil([() => 1])).to.equal(1);
    expect(utils.firstNonNil([() => 1, 3])).to.equal(1);
    expect(utils.firstNonNil([undefined, undefined, () => 3, 1])).to.equal(3);
  });
});

describe('observeOverload', () => {
  it('should invoke the callback, if there too many dispatches in a short time', done => {
    try {
      const dispatch = utils.observeOverload(() => true);

      for (let i = 0; i < 100; i += 1) {
        if (dispatch()) {
          return done();
        }
      }

      throw new Error('Callback not called');
    }
    catch (error) {
      done(error);
    }
  });

  it('should allow configuring the events limit', done => {
    try {
      for (let i = 1; i < 10; i += 1) {
        const dispatch = utils.observeOverload(() => done('Limit option is ignored'), { limit: 100 });
        for (let j = 0; j < i * 10; j += 1) {
          dispatch();
        }
      }

      // exit if we done, otherwise throw
      const dispatch = utils.observeOverload(done, { limit: 100 });

      for (let i = 0; i < 110; i += 1) {
        dispatch();
      }

      throw new Error('Limit option is ignored');
    }
    catch (error) {
      done(error);
    }
  });

  it('should not invoke callback, if time between calls longer then options.delay', done => {
    try {
      const dispatch = utils.observeOverload(() => done('Callback should not be called'), { delay: 100, limit: 2 });
      let count = 0;

      const id = setInterval(() => {
        dispatch();
        count += 1;
        if (count >= 3) {
          done();
          clearInterval(id);
        }
      }, 110);
    }
    catch (error) {
      done(error);
    }
  });
});