UNPKG

flowxo-utils

Version:
1,822 lines (1,560 loc) 58.8 kB
'use strict'; var util = require('util'), Utils = require('../lib/index.js'), _ = require('lodash'), SugarDate = require('sugar-date').Date, moment = require('moment'); describe('Utils', function() { it('should convert an array to a hashtable', function() { // A hashtable means you don't need to run array.indexOf. // http://jsperf.com/array-indexof-vs-hashtable-lookup var arr = ['test1', 'test2']; var hashtable = Utils.toHash(arr); expect(hashtable).toEqual({ test1: true, test2: true }); }); describe('Hash to Key Value pairs', function() { it('should convert a hashtable to an array of key-value pairs', function() { var actual = Utils.hashToKeyValPairs({ one: 'two', buckle: 'my shoe' }); expect(actual).toEqual([{ key: 'one', value: 'two' }, { key: 'buckle', value: 'my shoe' }]); }); it('should return an empty array if the hashtable is not defined', function() { var actual = Utils.hashToKeyValPairs(); expect(actual).toEqual([]); }); }); describe('Get Flattened Value', function() { it('should get a flattened property', function() { var data = { some: { flattened: { key: 'value' } } }; var actual = Utils.getFlattenedValue(data, 'some__flattened__key'); expect(actual).toBe('value'); }); it('should get a flattened property with a custom delimiter', function() { var data = { some: { flattened: { key: 'value' } } }; var actual = Utils.getFlattenedValue(data, 'some.flattened.key', '.'); expect(actual).toBe('value'); }); it('should get a flattened array', function() { var data = { some: [{ key: 'value' }] }; var actual = Utils.getFlattenedValue(data, 'some__0__key'); expect(actual).toBe('value'); }); it('should get a flattened property when an object key is a number', function() { var data = { some: { '0': { key: 'value' } } }; var actual = Utils.getFlattenedValue(data, 'some__0__key'); expect(actual).toBe('value'); }); it('should return an empty string property', function() { var data = { some: { '0': { key: '' } } }; var actual = Utils.getFlattenedValue(data, 'some__0__key'); expect(actual).toBe(''); }); it('should return a `false` as a property', function() { var data = { some: { '0': { key: false } } }; var actual = Utils.getFlattenedValue(data, 'some__0__key'); expect(actual).toBe(false); }); it('should return a `0` as a property', function() { var data = { some: { '0': { key: 0 } } }; var actual = Utils.getFlattenedValue(data, 'some__0__key'); expect(actual).toBe(0); }); it('should return null if the property is null', function() { var data = { some: { flattened: { key: null } } }; var actual = Utils.getFlattenedValue(data, 'some__flattened__key'); expect(actual).toBe(null); }); it('should return undefined if the property is undefined', function() { var data = { some: { flattened: { key: 'value' } } }; var actual = Utils.getFlattenedValue(data, 'some__incorrect'); expect(actual).toBe(undefined); }); it('should return undefined if the array index is invalid', function() { var data = { some: [{ key: 'value' }] }; var actual = Utils.getFlattenedValue(data, 'some__1__key'); expect(actual).toBe(undefined); }); it('should return null if the object is null', function() { var data = { some: { flattened: null } }; var actual = Utils.getFlattenedValue(data, 'some__flattened__key'); expect(actual).toBe(null); }); it('should return undefined if the object is undefined', function() { var data; var actual = Utils.getFlattenedValue(data, 'some__incorrect'); expect(actual).toBe(undefined); }); it('should get a flattened collection', function() { var data = { some: [{ key: 0 }, { key: 1 }, { key: 2 }] }; var options = { arrayFormatter: function() { return 'array-formatted'; } }; spyOn(options, 'arrayFormatter').and.callThrough(); var actual = Utils.getFlattenedValue(data, 'some_+_key', options); expect(options.arrayFormatter) .toHaveBeenCalledWith([ 'key' ], [{ key: 0 }, { key: 1 }, { key: 2 }], []); expect(actual).toBe('array-formatted'); }); it('should get a flattened multilevel collection', function() { var data = { some: [{ key: [{ id: 1 }, { id: 2 }] }, { key: [{ id: 3 }, { id: 4 }] }] }; var options = { arrayFormatter: function() { return 'array-formatted'; } }; spyOn(options, 'arrayFormatter').and.callThrough(); var actual = Utils.getFlattenedValue(data, 'some_+_key_+_id', options); expect(options.arrayFormatter) .toHaveBeenCalledWith([ 'key', 'id' ], [{ key: [{ id: 1 }, { id: 2 }] }, { key: [{ id: 3 }, { id: 4 }] }], []); expect(actual).toBe('array-formatted'); }); }); describe('Set Flattened Value', function() { it('should set a regular value', function() { var rtn = Utils.setFlattenedValue( {}, 'some', 'data'); expect(rtn).toEqual({ some: 'data' }); }); it('should set a flattened value', function() { var rtn = Utils.setFlattenedValue( {}, 'some__flattened', 'data'); expect(rtn).toEqual({ some: { flattened: 'data' } }); }); it('should set a flattened value with a custom delimiter', function() { var rtn = Utils.setFlattenedValue( {}, 'some.flattened', 'data', '.'); expect(rtn).toEqual({ some: { flattened: 'data' } }); }); it('should return the object if it is undefined', function() { var rtn = Utils.setFlattenedValue( null, 'some__flattened', 'data'); expect(rtn).toBeNull(); }); }); describe('Get Flattened Fields', function() { var options; // Set an array formatter for getting flattened collection fields beforeEach(function() { options = { arrayFormatter: function(keys, data) { if(!Array.isArray(data)) { data = Object.keys(data) .filter(function(key) { return !isNaN(key); }) .map(function(key) { return data[key]; }); } return data .map(function(item) { return _.reduce(keys, function(prev, key) { if(prev) { return prev[key]; } return null; }, item); }); } }; }); it('should return flattened fields for an object', function() { var data = { some: { nested: { key: 'value' } } }; var actual = Utils.getFlattenedFields(data); expect(actual).toEqual([{ key: 'some__nested__key', label: 'Some nested key', value: 'value' }]); }); it('should return flattened fields including an array', function() { var data = { some: [{ key: 'value' }] }; var actual = Utils.getFlattenedFields(data, options); expect(actual).toEqual([{ key: 'some_+_key', label: 'Some key', value: [ 'value' ] }, { key: 'some__0__key', label: 'Some 0 key', value: 'value' }]); }); it('should return flattened fields and honor the limit option', function() { var data = { some: [ {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'}, {key: 'value'} ] }; var actual = Utils.getFlattenedFields(data, {limit: 5}); expect(actual).toEqual([{ key: 'some_+_key', label: 'Some key', value: undefined }, { key: 'some__0__key', label: 'Some 0 key', value: 'value' }, { key: 'some__1__key', label: 'Some 1 key', value: 'value' }, { key: 'some__2__key', label: 'Some 2 key', value: 'value' }, { key: 'some__3__key', label: 'Some 3 key', value: 'value' }]); }); it('should return flattened fields for an object with a custom delimiter', function() { var data = { some: { nested: { key: 'value' } } }; var actual = Utils.getFlattenedFields(data, { delimiter: '.' }); expect(actual).toEqual([{ key: 'some.nested.key', label: 'Some nested key', value: 'value' }]); }); it('should return an empty array if an empty object was passed in', function() { var actual = Utils.getFlattenedFields({}); expect(actual).toEqual([]); }); it('should return an empty array if an object was not passed in', function() { var actual = Utils.getFlattenedFields(null); expect(actual).toEqual([]); }); it('should return a hashmap if specified', function() { var data = { some: { nested: { key: 'value' } } }; var actual = Utils.getFlattenedFields(data, { idx: true }); expect(actual).toEqual({ 'some__nested__key': { key: 'some__nested__key', label: 'Some nested key', value: 'value' } }); }); it('should return an empty hashmap if an empty object was passed in', function() { var actual = Utils.getFlattenedFields({}, { idx: true }); expect(actual).toEqual({}); }); it('should return an empty hashmap if an object was not passed in', function() { var actual = Utils.getFlattenedFields(null, { idx: true }); expect(actual).toEqual({}); }); it('should produce a field if an object key is a number', function() { // This is unsupported, as number keys // are reserved for array index use. var data = { some: { '0': { key: 'value' }, nested: { key: 'property' } } }; var actual = Utils.getFlattenedFields(data, options); expect(actual).toEqual([{ key: 'some_+_key', label: 'Some key', value: [ 'value' ] }, { key: 'some__0__key', label: 'Some 0 key', value: 'value' }, { key: 'some__nested__key', label: 'Some nested key', value: 'property' }]); }); it('should support objects with a length property', function() { var data = { some: 'data', length: 10, and: { more: { nested: 'data', length: { deeply: 'nested' } } } }; var actual = Utils.getFlattenedFields(data); expect(actual).toEqual([{ key: 'some', label: 'Some', value: 'data' }, { key: 'length', label: 'Length', value: 10 }, { key: 'and__more__nested', label: 'And more nested', value: 'data' }, { key: 'and__more__length__deeply', label: 'And more length deeply', value: 'nested' }]); }); it('should support collections', function() { var data = { some: [{ data: { key: 1 } }, { data: { key: 2 } }] }; var options = { arrayFormatter: function() { return { key: 'array-formatted' }; } }; var actual = Utils.getFlattenedFields(data, options); expect(actual).toEqual([{ key: 'some_+_data__key', label: 'Some data key', value: 'array-formatted' }, { key: 'some__0__data__key', label: 'Some 0 data key', value: 1 }, { key: 'some__1__data__key', label: 'Some 1 data key', value: 2 }]); }); }); describe('Get Flattened Fields Index', function() { it('should get flattened fields as an index', function() { spyOn(Utils, 'getFlattenedFields').and.returnValue('some_flattened_fields'); var data = 'data'; var options = { delimiter: '..' }; var actual = Utils.getFlattenedFieldsIdx(data, options); expect(actual).toBe('some_flattened_fields'); expect(Utils.getFlattenedFields).toHaveBeenCalledWith('data', { delimiter: '..', idx: true }); }); it('should accept a string as the option', function() { spyOn(Utils, 'getFlattenedFields').and.returnValue('some_flattened_fields'); var data = 'data'; var delimiter = '..'; var actual = Utils.getFlattenedFieldsIdx(data, delimiter); expect(actual).toBe('some_flattened_fields'); expect(Utils.getFlattenedFields).toHaveBeenCalledWith('data', { delimiter: '..', idx: true }); }); }); describe('Clone Terse', function() { var expectCloned = function(original, expected) { var cloned = Utils.cloneTerse(original); expect(cloned).not.toBe(original); expect(cloned).toEqual(expected); }; it('should remove nulls from an object', function() { var obj = { some: 'data', other: null, nested: { and: 'data', again: null } }; expectCloned(obj, { some: 'data', nested: { and: 'data' } }); }); it('should remove undefineds from an object', function() { var obj = { some: 'data', other: undefined, nested: { and: 'data', again: undefined } }; expectCloned(obj, { some: 'data', nested: { and: 'data' } }); }); it('should preserve empty strings, 0, and false', function() { var obj = { some: 'data', string: '', nested: { number: 0, boolean: false } }; expectCloned(obj, { some: 'data', string: '', nested: { number: 0, boolean: false } }); }); it('should remove nulls from an array', function() { var arr = [ 'data', null, null, 'data' ]; expectCloned(arr, [ 'data', 'data' ]); }); it('should remove undefineds from an array', function() { var arr = [ 'data', undefined, undefined, 'data' ]; expectCloned(arr, [ 'data', 'data' ]); }); it('should preserve empty strings, 0, and false', function() { var arr = [ '', 0, false ]; expectCloned(arr, [ '', 0, false ]); }); it('should remove any empty objects', function() { var obj = { some: 'data', nested: {} }; expectCloned(obj, { some: 'data' }); }); it('should remove any empty arrays', function() { var obj = { some: 'data', nested: [] }; expectCloned(obj, { some: 'data' }); }); it('should remove prototype properties', function() { function ToBeCloned() {} ToBeCloned.prototype.shouldNotBeCloned = function() {}; var toBeCloned = new ToBeCloned(); toBeCloned.willRemain = 'hello'; toBeCloned.willNotRemain = null; expectCloned(toBeCloned, { willRemain: 'hello' }); }); it('should tersify a complex object graph', function() { var obj = { some: 'data', other: false, removeme: null, nested_arr: [{ removeme: null }, { should: 'stay', and: 0, nested: [ '', { should: 'stay' }, 42 ] }], nested_obj: { arr: [ undefined ], again: [], another: ['keepme'], more_nesting: { and: 'more' } } }; expectCloned(obj, { some: 'data', other: false, nested_arr: [{ should: 'stay', and: 0, nested: [ '', { should: 'stay' }, 42 ] }], nested_obj: { another: ['keepme'], more_nesting: { and: 'more' } } }); }); it('should return null if object is completely tersed', function() { var obj = { some: null, nested: [] }; expectCloned(obj, null); }); }); // Date/Times with timezones get tricky. Take your time to understand it. // If it seems complicated it is. (Basically, there is a reason for the crazy tests) describe('Parse Date Time Field', function() { // I have chosen this method vs SugarDate.is() because of the nice output for debugging. function expectDatesToBeClose(value, expected, milliseconds) { milliseconds = milliseconds || 100; var diff = value.valueOf() - expected.valueOf(); if (diff > milliseconds || diff < (milliseconds * -1)) { fail('Expected ' + value + ' to be close to ' + expected + '.'); } } var expectValidDate = function(input, expected, options) { var parsedDate = Utils.parseDateTimeField(input, options); expect(parsedDate).toEqual({ type: 'date', input: input, valid: true, parsed: jasmine.any(Object), moment: jasmine.any(Object) }); expectDatesToBeClose(parsedDate.moment.toDate(), expected); expectDatesToBeClose(parsedDate.parsed, expected); }; var expectInvalidDate = function(input) { var parsedDate = Utils.parseDateTimeField(input); expect(parsedDate).toEqual({ type: 'date', input: input, valid: false, parsed: jasmine.any(Date) }); expect(SugarDate.isValid(parsedDate.parsed)).toBe(false); }; beforeEach(function() { spyOn(Utils, '_getFutureDate').and.callThrough(); }); it('should return the date if one is passed', function() { var date = new Date(); expectValidDate(date, date); }); it('should return a valid object if a number is passed', function() { expectValidDate(0, new Date(0)); expectValidDate(42, new Date(42)); }); it('should parse a datetime string', function() { expectValidDate('tomorrow', moment().add(1, 'days').startOf('day').toDate()); }); it('should return as invalid if no string is passed', function() { expectInvalidDate(''); }); it('should return as invalid if a string with whitespace is passed', function() { expectInvalidDate(' '); }); it('should return as invalid if the string points to an invalid date', function() { expectInvalidDate('invalid'); }); it('should handle a integer', function() { expectValidDate(1485410400000, new Date(1485410400000)); }); it('should handle a Date', function() { expectValidDate(new Date(1485410400000), new Date(1485410400000)); }); describe('Locales', function() { it('parses a date without locale', function() { var d = Utils.parseDateTimeField('now'); var expected = moment().toDate(); // We give a margin of 100ms expectDatesToBeClose(d.parsed, expected); expectDatesToBeClose(d.moment.toDate(), expected); }); it('parses a date with locale', function() { var d = Utils.parseDateTimeField('11/1/2017', {locale: 'en-GB', timezone: 'UTC'}); expect(d.moment.toISOString()).toEqual('2017-01-11T00:00:00.000Z'); // .parsed is expected to be in the timezone of the host machine. expect(d.parsed).toEqual(moment('2017-01-11').toDate()); d = Utils.parseDateTimeField('11/1/2017', {locale: 'en', timezone: 'UTC'}); expect(d.moment.toISOString()).toEqual('2017-11-01T00:00:00.000Z'); // .parsed is expected to be in the timezone of the host machine. expect(d.parsed).toEqual(moment('2017-11-01').toDate()); }); it('defaults locale when timezone is Europe/London', function() { var d = Utils.parseDateTimeField('now', {timezone: 'Europe/London'}); var expected = moment().tz('Europe/London').toDate(); expectDatesToBeClose(d.moment.toDate(), expected); // .parsed is expected to be in the timezone of the host machine. expectDatesToBeClose(d.parsed, moment().toDate()); }); }); describe('Timezones', function() { it('parses a date without timezone', function() { var d = Utils.parseDateTimeField('2017-01-11'); expect(d.moment.toISOString()).toEqual(moment('2017-01-11').toISOString()); // .parsed is expected to be in the timezone of the host machine. expect(d.parsed).toEqual(moment('2017-01-11').toDate()); }); it('parses a date with timezone', function() { var d = Utils.parseDateTimeField('1/11/2017', {timezone: 'America/New_York'}); expect(d.moment.format()).toEqual('2017-01-11T00:00:00-05:00'); // .parsed is expected to be in the timezone of the host machine. expect(d.parsed).toEqual(moment('2017-01-11').toDate()); }); it('should assume the correct day', function() { function checkTime(time, today, timezone) { var testString = today ? 'today ' + time : time; var d = Utils.parseDateTimeField(testString, {timezone: timezone}); var expectedMoment = moment.tz(time, 'HHa', timezone); var expectedParsed = moment(time, 'HHa'); // This duplicates the future feature of SugarDate if (!today && moment().tz(timezone).isAfter(expectedMoment)) { expectedMoment.add(1, 'days'); } if (!today && moment().isAfter(expectedParsed)) { expectedParsed.add(1, 'days'); } expect(d.moment.toDate()).toEqual(expectedMoment.toDate()); // .parsed is expected to be in the timezone of the host machine. expect(d.parsed).toEqual(expectedParsed.toDate()); } [ '12am', '2am', '4am', '6am', '8am', '10am', '12pm', '1pm', '3pm', '5pm', '7pm', '9pm', '11pm' ].forEach(function(time) { checkTime(time, false, 'America/Chicago'); checkTime(time, false, 'UTC'); checkTime(time, true, 'America/Chicago'); checkTime(time, true, 'UTC'); }); }); it('it should honor the timezone when parsing a relative date', function() { function checkRelative(timezone) { var d = Utils.parseDateTimeField('in 5 minutes', {timezone: timezone}); var expectedMoment = moment().tz(timezone).add(5, 'minutes'); expectDatesToBeClose(d.moment.toDate(), expectedMoment.toDate()); var expectedParsed = moment().add(5, 'minutes'); expectDatesToBeClose(d.parsed, expectedParsed.toDate()); } checkRelative('America/Chicago'); checkRelative('Asia/Tokyo'); checkRelative('UTC'); }); }); describe('Offset Modifiers', function() { it('should strip offset modifier from string to parse', function() { Utils.parseDateTimeField('now +1d'); expect(Utils._getFutureDate).toHaveBeenCalledWith('now', jasmine.any(Object)); }); describe('Increment', function() { it('should increment datetime field by days offset modifier', function() { expectValidDate('now +1d', moment().add(1, 'days').toDate()); }); it('should increment datetime field by hours offset modifier', function() { expectValidDate('now +30h', moment().add(30, 'hours').toDate()); }); it('should increment datetime field by minutes offset modifier', function() { expectValidDate('now +90m', moment().add(90, 'minutes').toDate()); }); it('should increment datetime field by seconds offset modifier', function() { expectValidDate('now +100s', moment().add(100, 'seconds').toDate()); }); }); describe('Decrement', function() { it('should decrement datetime field by days offset modifier', function() { expectValidDate('now -1d', moment().subtract(1, 'days').toDate()); }); it('should decrement datetime field by hours offset modifier', function() { expectValidDate('now -30h', moment().subtract(30, 'hours').toDate()); }); it('should decrement datetime field by minutes offset modifier', function() { expectValidDate('now -90m', moment().subtract(90, 'minutes').toDate()); }); it('should decrement datetime field by seconds offset modifier', function() { expectValidDate('now -100s', moment().subtract(100, 'seconds').toDate()); }); }); it('should parse a complex offset modifier #1', function() { var expected = moment() .add(5, 'days') .add(4, 'hours') .add(30, 'minutes') .add(15, 'seconds') .toDate(); expectValidDate('now +5d +4h +30m +15s', expected); }); it('should parse a complex offset modifier #2', function() { var expected = moment() .subtract(5, 'days') .subtract(4, 'hours') .subtract(30, 'minutes') .subtract(15, 'seconds') .toDate(); expectValidDate('now -5d -4h -30m -15s', expected); }); it('should parse a complex offset modifier #3', function() { var expected = moment() .add(3, 'days') .subtract(900, 'minutes') .toDate(); expectValidDate('now +3d -900m', expected); }); it('should parse a complex offset modifier #4', function() { var expected = moment() .add(40, 'hours') .subtract(30000, 'seconds') .toDate(); expectValidDate('now +40h -30000s', expected); }); it('should parse a complex offset modifier #5', function() { var expected = moment() .add(40, 'hours') .subtract(40, 'hours') .toDate(); expectValidDate('now +40h -40h', expected); }); it('should parse a complex offset modifier #6', function() { var expected = moment() .add(40, 'hours') .subtract(40, 'hours') .toDate(); expectValidDate('+40h now - 40h', expected); }); it('shouldn\'t allow two consecutive offset operators', function() { expectInvalidDate('tomorrow++1d'); }); it('shouldn\'t allow two consecutive offset operators separated by whitespace', function() { expectInvalidDate('tomorrow+ +1d'); }); it('should allow no whitespace before and after an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow+1d', expected); }); it('should allow one space before an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow +1d', expected); }); it('should allow one space after an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow+ 1d', expected); }); it('should allow one space before and after an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow + 1d', expected); }); it('should allow arbitrary whitespace before an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow +1d', expected); }); it('should allow arbitrary whitespace after an offset modifier', function() { var expected = moment() .add(2, 'days') .startOf('day') .toDate(); expectValidDate('tomorrow+ 1d', expected); }); it('should start from the current date if offset modifiers are only present', function() { var expected = moment() .add(1, 'days') .toDate(); expectValidDate('+1d', expected); }); it('should not apply the offset modifier if it is 0', function() { spyOn(SugarDate, 'addSeconds'); Utils.parseDateTimeField('now'); expect(SugarDate.addSeconds) .not.toHaveBeenCalled(); }); it('should not apply the offset modifier if the date is invalid', function() { spyOn(SugarDate, 'addSeconds'); Utils.parseDateTimeField('some_invalid_date +40h'); expect(SugarDate.addSeconds) .not.toHaveBeenCalled(); }); describe('Timezone offsets', function() { it('should work with the YYYY-MM-DD HH-mmZ format', function() { var expected = moment('2013-02-08T09:30:00+07:00').toDate(); expectValidDate('2013-02-08 09:30+07:00', expected); }); it('should work with the YYYY-MM-DD HH-mmZZ format', function() { var expected = moment('2013-02-08T09:30:00-01:00').toDate(); expectValidDate('2013-02-08 09:30-0100', expected); }); it('should work with the YYYY-MM-DD HH:mm:ss.SSSZ format', function() { var expected = moment('2013-02-08T09:30:26.123+07:00').toDate(); expectValidDate('2013-02-08 09:30:26.123+07:00', expected); }); it('should work with the ddd, DD MMM YYYY HH:mm:ss ZZ format', function() { var expected = moment('1995-12-25T13:30:00+04:30').toDate(); expectValidDate('Mon, 25 Dec 1995 13:30:00 +0430', expected); }); it('should work with the YYYY-MM-DDTHH:mm:ssZZ format', function() { var expected = moment('1995-12-25T13:30:00+04:30').toDate(); expectValidDate('1995-12-25T13:30:00+0430', expected); }); it('should work with the YYYY-MM-DD HH-mmZ format and modifiers', function() { var expected = moment('2013-02-08T21:30:00+07:00').toDate(); expectValidDate('2013-02-08 09:30+07:00 +1d - 12h', expected); }); it('should work with the YYYY-MM-DD HH-mmZZ format and modifiers', function() { var expected = moment('2013-02-08T21:30:00-01:00').toDate(); expectValidDate('2013-02-08 09:30-0100 +1d - 12h', expected); }); it('should work with the YYYY-MM-DD HH:mm:ss.SSSZ format and modifiers', function() { var expected = moment('2013-02-08T21:30:26.123+07:00').toDate(); expectValidDate('2013-02-08 09:30:26.123+07:00 +1d - 12h', expected); }); it('should work with the ddd, DD MMM YYYY HH:mm:ss ZZ format and modifiers', function() { var expected = moment('1995-12-26T01:30:00+04:30').toDate(); expectValidDate('Mon, 25 Dec 1995 13:30:00 +0430 +1d - 12h', expected); }); it('should work with the YYYY-MM-DDTHH:mm:ssZZ format and modifiers', function() { var expected = moment('1995-12-26T01:30:00+04:30').toDate(); expectValidDate('1995-12-25T13:30:00+0430 +1d - 12h', expected); }); it('should work with timezone', function() { var date = Utils.parseDateTimeField('2016-7-22T00:00:00+01:00', {timezone: 'Europe/London'}); expect(date.moment.toISOString()).toEqual('2016-07-21T23:00:00.000Z'); }); }); }); describe('Apply Timezone Offset', function() { it('should return a date in the correct offset', function() { var d; d = Utils.applyTzOffset(new Date('2017-02-14'), 'America/Chicago'); expect(d).toEqual(moment.tz('2017-02-14', 'America/Chicago').toDate()); expect(d.toISOString()).toEqual('2017-02-14T06:00:00.000Z'); d = Utils.applyTzOffset(new Date('2017-02-15'), 'America/New_York'); expect(d).toEqual(moment.tz('2017-02-15', 'America/New_York').toDate()); expect(d.toISOString()).toEqual('2017-02-15T05:00:00.000Z'); d = Utils.applyTzOffset(new Date('2017-02-16'), 'Europe/London'); expect(d).toEqual(moment.tz('2017-02-16', 'Europe/London').toDate()); expect(d.toISOString()).toEqual('2017-02-16T00:00:00.000Z'); d = Utils.applyTzOffset(new Date(), 'Europe/London'); expectDatesToBeClose(d, moment().tz('Europe/London').toDate()); }); }); }); describe('Parse Boolean Field', function() { var expectTrue = function(input) { expect(Utils.parseBooleanField(input)).toEqual({ type: 'boolean', input: input, valid: true, parsed: true }); }; var expectFalse = function(input) { expect(Utils.parseBooleanField(input)).toEqual({ type: 'boolean', input: input, valid: true, parsed: false }); }; var expectInvalidBoolean = function(input) { expect(Utils.parseBooleanField(input)).toEqual({ type: 'boolean', input: input, valid: false, parsed: null }); }; it('should return a passed boolean', function() { expectTrue(true); expectFalse(false); }); describe('Number parsing', function() { it('should return `true` if `1` is passed', function() { expectTrue(1); }); it('should return `false` if `0` is passed', function() { expectFalse(0); }); it('should return as invalid if another number is passed', function() { expectInvalidBoolean(2); expectInvalidBoolean(102); expectInvalidBoolean(-500); expectInvalidBoolean(3.141); expectInvalidBoolean(NaN); expectInvalidBoolean(Infinity); }); }); describe('String parsing', function() { it('should return `true` if `true` is passed', function() { expectTrue('true'); }); it('should return `true` if `yes` is passed', function() { expectTrue('yes'); }); it('should return `true` if `y` is passed', function() { expectTrue('y'); }); it('should return `true` if `1` is passed', function() { expectTrue('1'); }); it('should return `false` if `false` is passed', function() { expectFalse('false'); }); it('should return `false` if `no` is passed', function() { expectFalse('no'); }); it('should return `false` if `n` is passed', function() { expectFalse('n'); }); it('should return `false` if `0` is passed', function() { expectFalse('0'); }); it('should return as invalid if another string is passed', function() { expectInvalidBoolean('foo'); expectInvalidBoolean('2'); expectInvalidBoolean('ok'); expectInvalidBoolean('oui'); expectInvalidBoolean(''); }); it('should match strings case insensitively', function() { expectTrue('true'); expectTrue('TRUE'); expectTrue('True'); expectTrue('yEs'); expectTrue('Y'); expectFalse('FalsE'); expectFalse('FALSE'); expectFalse('No'); expectFalse('NO'); expectFalse('N'); }); }); it('should return as invalid if another type is passed', function() { expectInvalidBoolean(new Date()); expectInvalidBoolean({}); expectInvalidBoolean([]); expectInvalidBoolean(/^$/); }); }); describe('Annotate Script Input Data', function() { var methodFields; beforeEach(function() { methodFields = [{ key: 'field', label: 'Field' }, { key: 'nested__field', label: 'Nested Field' }]; }); it('should return an empty array if the method fields are null', function() { var scriptData = { field: 'Value' }; methodFields = null; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should return an empty array if the method fields are empty', function() { var scriptData = { field: 'Value' }; methodFields = []; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should annotate data using labels from method fields', function() { var scriptData = { field: 'Value' }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: 'Value' }]); }); it('should include empty fields if required', function() { var annotate = function(scriptData) { return Utils.annotateInputData(scriptData, methodFields, { includeEmptyFields: true }); }; expect(annotate({})).toEqual([{ key: 'field', label: 'Field', value: undefined }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: undefined })).toEqual([{ key: 'field', label: 'Field', value: undefined }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: null })).toEqual([{ key: 'field', label: 'Field', value: null }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: '' })).toEqual([{ key: 'field', label: 'Field', value: '' }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); }); it('should discard empty fields if required', function() { var annotate = function(scriptData) { return Utils.annotateInputData(scriptData, methodFields, { includeEmptyFields: false }); }; expect(annotate({})).toEqual([]); expect(annotate({ field: undefined })).toEqual([]); expect(annotate({ field: null })).toEqual([]); expect(annotate({ field: '' })).toEqual([]); }); it('should annotate a datetime field', function() { methodFields[0].type = 'datetime'; var scriptData = { field: { type: 'datetime', input: 'today', valid: true, parsed: new Date() } }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: 'today' }]); }); it('should handle a non-existant datetime value', function() { methodFields[0].type = 'datetime'; var scriptData = { field: null }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should annotate a boolean field', function() { methodFields[0].type = 'boolean'; var scriptData = { field: { type: 'boolean', input: 'yes', valid: true, parsed: true } }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: 'yes' }]); }); it('should handle a non-existant boolean value', function() { methodFields[0].type = 'boolean'; var scriptData = { field: null }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should annotate a select field', function() { methodFields[0].type = 'select'; methodFields[0].input_options = [{ label: 'Option', value: 'option' }]; var scriptData = { field: 'option' }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: 'Option' }]); }); it('should annotate a dictionary field', function() { methodFields[0].type = 'dictionary'; var scriptData = { field: { key1: 'value1', key2: 'value2' } }; var actual = Utils.annotateInputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: '{\n "key1": "value1",\n "key2": "value2"\n}' }]); }); }); describe('Annotate Script Output Data', function() { var methodFields; beforeEach(function() { methodFields = [{ key: 'field', label: 'Field' }, { key: 'nested__field', label: 'Nested Field' }]; }); it('should return an empty array if the method fields are undefined', function() { var scriptData = { field: 'Value' }; methodFields = null; var actual = Utils.annotateOutputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should return an empty array if the method fields are empty', function() { var scriptData = { field: 'Value' }; methodFields = []; var actual = Utils.annotateOutputData(scriptData, methodFields); expect(actual).toEqual([]); }); it('should annotate data using labels from method fields', function() { var scriptData = { field: 'Value' }; var actual = Utils.annotateOutputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'field', label: 'Field', value: 'Value' }]); }); it('should annotate nested data using labels from method fields', function() { var scriptData = { nested: { field: 'Value' } }; var actual = Utils.annotateOutputData(scriptData, methodFields); expect(actual).toEqual([{ key: 'nested__field', label: 'Nested Field', value: 'Value' }]); }); it('should include empty fields if required', function() { var annotate = function(scriptData) { return Utils.annotateOutputData(scriptData, methodFields, { includeEmptyFields: true }); }; expect(annotate({})).toEqual([{ key: 'field', label: 'Field', value: undefined }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: undefined })).toEqual([{ key: 'field', label: 'Field', value: undefined }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: null })).toEqual([{ key: 'field', label: 'Field', value: null }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); expect(annotate({ field: '' })).toEqual([{ key: 'field', label: 'Field', value: '' }, { key: 'nested__field', label: 'Nested Field', value: undefined }]); }); it('should discard empty fields if required', function() { var annotate = function(scriptData) { return Utils.annotateOutputData(scriptData, methodFields, { includeEmptyFields: false }); }; expect(annotate({})).toEqual([]); expect(annotate({ field: undefined })).toEqual([]); expect(annotate({ field: null })).toEqual([]); expect(annotate({ field: '' })).toEqual([]); }); }); describe('Backoff', function() { describe('attempt', function() { it('should perform the operation once if it succeeds', function(done) { var attempts = 0; var work = function(done) { attempts++; done(null, 'result1', 'result2'); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 3 }; Utils.Backoff.attempt(work, options, function(err, res1, res2) { expect(err).toBeNull(); expect(res1).toBe('result1'); expect(res2).toBe('result2'); expect(attempts).toBe(1); done(); }); }); it('should retry the operation with backoff if it fails', function(done) { var attempts = 0; var work = function(done) { attempts++; done(attempts < 3 ? 'ERROR' : null, 'result1', 'result2'); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 3 }; var start = Date.now(); Utils.Backoff.attempt(work, options, function(err, res1, res2) { var duration = Date.now() - start; expect(duration).toBeGreaterThan(29); expect(err).toBeNull(); expect(res1).toBe('result1'); expect(res2).toBe('result2'); expect(attempts).toBe(3); done(); }); }); it('should retry the operation with random backoff if it fails', function(done) { var attempts = 0; var work = function(done) { attempts++; done(attempts < 3 ? 'ERROR' : null, 'result1', 'result2'); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 3, useRandom: true }; var start = Date.now(); Utils.Backoff.attempt(work, options, function(err, res1, res2) { var duration = Date.now() - start; expect(duration).toBeGreaterThan(29); expect(err).toBeNull(); expect(res1).toBe('result1'); expect(res2).toBe('result2'); expect(attempts).toBe(3); done(); }); }); it('should fail the operation if the maximum attempts is reached', function(done) { var attempts = 0; var work = function(done) { attempts++; done('ERROR'); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 3 }; var start = Date.now(); Utils.Backoff.attempt(work, options, function(err) { var duration = Date.now() - start; expect(duration).toBeGreaterThan(29); expect(err).toBe('ERROR'); expect(attempts).toBe(3); done(); }); }); it('should fail the operation if the maximum duration is reached', function(done) { var attempts = 0; var work = function(done) { attempts++; done('ERROR'); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 5, maxDuration: 40 }; var start = Date.now(); Utils.Backoff.attempt(work, options, function(err) { var duration = Date.now() - start; expect(duration).toBeGreaterThan(29); expect(err).toBe('ERROR'); expect(attempts).toBe(3); done(); }); }); it('should fail the operation if a NonRetryableError occurs', function(done) { var attempts = 0; var nrtErr = new Utils.Backoff.NonRetryableError('ERROR'); var work = function(done) { attempts++; done(nrtErr); }; var options = { minDelay: 10, maxDelay: 20, maxAttempts: 3 }; Utils.Backoff.attempt(work, options, function(err) { expect(err).toBe(nrtErr); expect(attempts).toBe(1); done(); }); }); it('should fail the operation if a custom NonRetryableError occurs', function(done) { function NonRetryableError() {} util.inherits(NonRetryableError, Error); var attempts = 0; var nrtErr = new NonRetryableError('ERROR'); var work = function(done) { attempts++; done(nrtErr);