add lifecycleBase unit tests

This commit is contained in:
Rene
2020-12-10 00:38:14 +01:00
parent 9f45348f3d
commit fa956c4b1d
5 changed files with 288 additions and 11 deletions
@@ -8,6 +8,8 @@ import {
validateOptions,
assignDeep,
createCache,
isEmptyObject,
isBoolean,
} from 'support';
import { PlainObject } from 'typings';
@@ -44,17 +46,27 @@ export const createLifecycleBase = <O, C>(
updateFunction: (changedOptions: OptionsValidated<O>, changedCache: CacheUpdated<C>) => any
): LifecycleBase<O, C> => {
const { _template: optionsTemplate, _options: defaultOptions } = transformOptions<Required<O>>(defaultOptionsWithTemplate);
const options: Required<O> = assignDeep({}, defaultOptions, validateOptions<O>(initialOptions || ({} as O), optionsTemplate)._validated);
const options: Required<O> = assignDeep(
{},
defaultOptions,
validateOptions<O>(initialOptions || ({} as O), optionsTemplate, null, true)._validated
);
const cacheChange = createCache<C>(cacheUpdateInfo);
const update = (hints: LifecycleUpdateHints<O, C>) => {
const hasForce = isBoolean(hints._force);
const force = hints._force === true;
const changedCache = cacheChange(force ? undefined : hints._changedCache, force);
const changedOptions = force ? ({} as O) : hints._changedOptions || ({} as O);
updateFunction(changedOptions, changedCache);
const changedCache = cacheChange(force ? null : hints._changedCache || (hasForce ? null : []), force);
const changedOptions = force ? options : hints._changedOptions || ({} as O);
if (!isEmptyObject(changedOptions) || !isEmptyObject(changedCache)) {
updateFunction(changedOptions, changedCache);
}
};
update({ _force: true });
return {
_options(newOptions?: O) {
if (newOptions) {
@@ -66,7 +78,7 @@ export const createLifecycleBase = <O, C>(
return options;
},
_update: (force?: boolean) => {
update({ _force: force });
update({ _force: !!force });
},
_cacheChange: (cachePropsToUpdate?: CachePropsToUpdate<C>) => {
update({ _changedCache: cachePropsToUpdate });
+2 -2
View File
@@ -20,7 +20,7 @@ type EqualCachePropFunction<T, P extends keyof T> = (a?: T[P], b?: T[P]) => bool
export type CachePropsToUpdate<T> = Array<keyof T> | keyof T;
export type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T>, force?: boolean) => CacheUpdated<T>;
export type CacheUpdate<T> = (propsToUpdate?: CachePropsToUpdate<T> | null, force?: boolean) => CacheUpdated<T>;
export type CacheUpdated<T> = {
[P in keyof T]?: T[P];
@@ -77,7 +77,7 @@ export const createCache = <T>(cacheUpdateInfo: CacheUpdateInfo<T>): CacheUpdate
return result;
};
return (propsToUpdate?: CachePropsToUpdate<T>, force?: boolean) => {
return (propsToUpdate, force) => {
const finalPropsToUpdate: Array<keyof T> =
(isString(propsToUpdate) ? ([propsToUpdate] as Array<keyof T>) : (propsToUpdate as Array<keyof T>)) || allProps;
each(finalPropsToUpdate, (prop) => {
@@ -1,4 +1,4 @@
import { each, indexOf, hasOwnProperty, keys } from 'support/utils';
import { each, hasOwnProperty, keys } from 'support/utils';
import { type, isArray, isUndefined, isEmptyObject, isPlainObject, isString } from 'support/utils/types';
import { OptionsTemplate, OptionsTemplateTypes, OptionsTemplateType, Func, OptionsValidationResult, OptionsValidated } from 'support/options';
import { PlainObject } from 'typings';
@@ -80,7 +80,13 @@ const validateRecursive = <T extends PlainObject>(
each(templateValueArr, (currTemplateType) => {
// if currType value isn't inside possibleTemplateTypes we assume its a enum string value
const isEnumString = indexOf(Object.values(optionsTemplateTypes), currTemplateType) < 0;
let typeString: string | undefined;
each(optionsTemplateTypes, (value: string, key: string) => {
if (value === currTemplateType) {
typeString = key;
}
});
const isEnumString = typeString === undefined;
if (isEnumString && isString(optionsValue)) {
// split it into a array which contains all possible values for example: ["yes", "no", "maybe"]
const enumStringSplit = currTemplateType.split(' ');
@@ -93,7 +99,7 @@ const validateRecursive = <T extends PlainObject>(
}
// build error message
errorPossibleTypes.push(isEnumString ? optionsTemplateTypes.string : currTemplateType);
errorPossibleTypes.push(isEnumString ? optionsTemplateTypes.string : typeString!);
// continue if invalid, break if valid
return !isValid;
@@ -143,7 +149,7 @@ const validateRecursive = <T extends PlainObject>(
const validateOptions = <T extends PlainObject>(
options: T,
template: OptionsTemplate<Required<T>>,
optionsDiff?: T,
optionsDiff?: T | null,
doWriteErrors?: boolean
): OptionsValidationResult<T> => {
/*
@@ -0,0 +1,242 @@
import { optionsTemplateTypes as oTypes } from 'support';
import { createLifecycleBase } from 'lifecycles/lifecycleBase';
interface TestLifecycleOptions {
number?: number;
string?: string;
nested?: {
boolean?: boolean;
number?: number;
};
}
interface TestLifecycleCache {
number?: number;
constant?: boolean;
object?: {
string?: string;
boolean?: boolean;
};
}
const createLifecycle = (initalOptions?: TestLifecycleOptions, updateFn?: () => any) =>
createLifecycleBase<TestLifecycleOptions, TestLifecycleCache>(
{
number: [0, oTypes.number],
string: ['hi', oTypes.string],
nested: {
boolean: [false, oTypes.boolean],
number: [0, oTypes.number],
},
},
{
number: (current) => (current || 0) + 1,
constant: () => false,
object: (current) => ({ string: `${current?.string || ''}hi`, boolean: !current?.boolean }),
},
initalOptions,
updateFn || (() => {})
);
describe('lifecycleBase', () => {
describe('options', () => {
test('correct default options', () => {
const { _options } = createLifecycle();
const defaultOptions = _options();
expect(defaultOptions.number).toBe(0);
expect(defaultOptions.string).toBe('hi');
expect(defaultOptions.nested?.boolean).toBe(false);
expect(defaultOptions.nested?.number).toBe(0);
});
test('correct initial options', () => {
const { _options } = createLifecycle({ number: 1, nested: { boolean: true } });
const initOptions = _options();
expect(initOptions.number).toBe(1);
expect(initOptions.string).toBe('hi');
expect(initOptions.nested?.boolean).toBe(true);
expect(initOptions.nested?.number).toBe(0);
});
test('correct options change', () => {
const { _options } = createLifecycle();
const options = _options();
expect(options.number).toBe(0);
expect(options.string).toBe('hi');
expect(options.nested?.boolean).toBe(false);
expect(options.nested?.number).toBe(0);
const changedOptions = _options({ number: 2, nested: { number: 3 } });
expect(changedOptions.number).toBe(2);
expect(changedOptions.string).toBe('hi');
expect(changedOptions.nested?.boolean).toBe(false);
expect(changedOptions.nested?.number).toBe(3);
});
test('correct options validation', () => {
const originalWarn = console.warn;
const mockWarn = jest.fn();
console.warn = mockWarn;
// @ts-ignore
const { _options } = createLifecycle({ string: 123 });
expect(mockWarn).toBeCalledTimes(1);
const options = _options();
expect(options.string).toBe('hi');
// @ts-ignore
const changedOptions = _options({ number: 'string', nested: null });
expect(mockWarn).toBeCalledTimes(2);
expect(changedOptions.number).toBe(0);
expect(changedOptions.string).toBe('hi');
expect(changedOptions.nested?.boolean).toBe(false);
expect(changedOptions.nested?.number).toBe(0);
console.warn = originalWarn;
});
});
describe('cache', () => {
test('single value cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
_cacheChange('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 });
_cacheChange('constant');
expect(updateFn).toBeCalledTimes(2);
});
test('multiple value cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
_cacheChange(['number', 'object']);
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2, object: { string: 'hihi', boolean: false } });
_cacheChange(['number', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3 });
_cacheChange(['constant']);
expect(updateFn).toBeCalledTimes(3);
});
});
describe('update', () => {
test('initial call', () => {
const updateFn = jest.fn();
createLifecycle({}, updateFn);
expect(updateFn).toBeCalledTimes(1);
expect(updateFn).toBeCalledWith(
{
number: 0,
string: 'hi',
nested: {
boolean: false,
number: 0,
},
},
{
number: 1,
constant: false,
object: {
string: 'hi',
boolean: true,
},
}
);
});
test('updates correctly on options change', () => {
const updateFn = jest.fn();
const { _options } = createLifecycle({}, updateFn);
_options({ number: 5 });
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({ number: 5 }, {});
_options({ number: 5, string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({ string: 'test', nested: { number: 3 } }, {});
_options({ number: 5, string: 'test', nested: { number: 3 } });
expect(updateFn).toBeCalledTimes(3);
});
test('updates correctly on cache change', () => {
const updateFn = jest.fn();
const { _cacheChange } = createLifecycle({}, updateFn);
_cacheChange('number');
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2 });
_cacheChange(['number', 'object', 'constant']);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith({}, { number: 3, object: { string: 'hihi', boolean: false } });
_cacheChange('constant');
expect(updateFn).toBeCalledTimes(3);
});
test('updates correctly on update call', () => {
const updateFn = jest.fn();
const { _update, _options } = createLifecycle({}, updateFn);
_update();
expect(updateFn).toBeCalledTimes(2);
expect(updateFn).toBeCalledWith({}, { number: 2, object: { string: 'hihi', boolean: false } });
_update(true);
expect(updateFn).toBeCalledTimes(3);
expect(updateFn).toBeCalledWith(
{
number: 0,
string: 'hi',
nested: {
boolean: false,
number: 0,
},
},
{
number: 3,
constant: false,
object: {
string: 'hihihi',
boolean: true,
},
}
);
_options({ number: 3, nested: { boolean: true } });
_update(true);
expect(updateFn).toBeCalledTimes(5);
expect(updateFn).toBeCalledWith(
{
number: 3,
string: 'hi',
nested: {
boolean: true,
number: 0,
},
},
{
number: 4,
constant: false,
object: {
string: 'hihihihi',
boolean: false,
},
}
);
});
});
});
@@ -201,5 +201,22 @@ describe('cache', () => {
expect(updateNumberFn).toHaveBeenCalledWith(2, 1);
expect(Object.prototype.hasOwnProperty.call(updateCache('number'), 'number')).toBe(false);
});
test('updates all entries with null or undefined as argument', () => {
const [updateNumberFn, updateNumber] = createUpdater<number>((i) => i);
const [updateNumberFn2, updateNumber2] = createUpdater<number>((i) => i);
const updateCache = createCache({
number: updateNumber,
number2: updateNumber2,
});
updateCache();
expect(updateNumberFn).toHaveBeenCalledWith(undefined, undefined);
expect(updateNumberFn2).toHaveBeenCalledWith(undefined, undefined);
updateCache(null);
expect(updateNumberFn).toHaveBeenCalledWith(1, undefined);
expect(updateNumberFn2).toHaveBeenCalledWith(1, undefined);
});
});
});